diff --git a/CHANGELOG b/CHANGELOG
index dbd5d4e9a..581fa69ef 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,13 @@
+Version 5.3.0 (2023-06-30)
+--------------------------
+Add media controller with APIs to track media events (#606)
+Add emitter configuration support to remote configuration (#607)
+Use default configuration for properties that are not configured using remote configuration (#613)
+Add custom HTTP headers configuration (#276)
+Truncate language in platform context entity to max 8 characters (#621)
+Truncate URL scheme for page_url and page_refr properties (#616)
+Remember requestCallback, customRetryForStatusCodes and onSessionUpdate set for initialized trackers after configuration updates
+
Version 5.2.0 (2023-06-02)
--------------------------
Track install referrer details entity along with the application install event if available (#249)
diff --git a/VERSION b/VERSION
index 91ff57278..03f488b07 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-5.2.0
+5.3.0
diff --git a/build.gradle b/build.gradle
index 52de0308b..ab888bd28 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,7 +22,7 @@ plugins {
subprojects {
group = 'com.snowplowanalytics'
- version = '5.2.0'
+ version = '5.3.0'
repositories {
google()
maven {
diff --git a/gradle.properties b/gradle.properties
index d63651f6b..99c126554 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -31,7 +31,7 @@ systemProp.org.gradle.internal.http.socketTimeout=120000
SONATYPE_STAGING_PROFILE=comsnowplowanalytics
GROUP=com.snowplowanalytics
POM_ARTIFACT_ID=snowplow-android-tracker
-VERSION_NAME=5.2.0
+VERSION_NAME=5.3.0
POM_NAME=snowplow-android-tracker
POM_PACKAGING=aar
diff --git a/snowplow-demo-java/src/main/java/com/snowplowanalytics/snowplowtrackerdemojava/Demo.java b/snowplow-demo-java/src/main/java/com/snowplowanalytics/snowplowtrackerdemojava/Demo.java
index 71aa47e99..31919b177 100644
--- a/snowplow-demo-java/src/main/java/com/snowplowanalytics/snowplowtrackerdemojava/Demo.java
+++ b/snowplow-demo-java/src/main/java/com/snowplowanalytics/snowplowtrackerdemojava/Demo.java
@@ -56,7 +56,6 @@
import com.snowplowanalytics.snowplow.emitter.BufferOption;
import com.snowplowanalytics.snowplow.globalcontexts.GlobalContext;
import com.snowplowanalytics.snowplow.tracker.DevicePlatform;
-import com.snowplowanalytics.snowplow.tracker.InspectableEvent;
import com.snowplowanalytics.snowplow.tracker.LoggerDelegate;
import com.snowplowanalytics.snowplow.network.HttpMethod;
import com.snowplowanalytics.snowplow.network.RequestCallback;
diff --git a/snowplow-demo-kotlin/src/main/AndroidManifest.xml b/snowplow-demo-kotlin/src/main/AndroidManifest.xml
index d8744ade7..63d5ec095 100644
--- a/snowplow-demo-kotlin/src/main/AndroidManifest.xml
+++ b/snowplow-demo-kotlin/src/main/AndroidManifest.xml
@@ -1,37 +1,38 @@
+ package="com.snowplowanalytics.snowplowdemokotlin">
-
-
-
+
+ android:usesCleartextTraffic="true">
+
+ android:exported="true"
+ android:screenOrientation="fullSensor">
+
-
+ android:screenOrientation="fullSensor">
diff --git a/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt
index 875e809b8..4cc7c86ca 100644
--- a/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt
+++ b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/Demo.kt
@@ -15,6 +15,7 @@ package com.snowplowanalytics.snowplowdemokotlin
import android.Manifest
import android.app.Activity
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
@@ -29,6 +30,7 @@ import androidx.core.content.ContextCompat
import androidx.core.util.Consumer
import androidx.core.util.Pair
import androidx.preference.PreferenceManager
+import com.snowplowanalytics.core.tracker.Logger
import com.snowplowanalytics.core.utils.Util
import com.snowplowanalytics.snowplow.Snowplow.createTracker
import com.snowplowanalytics.snowplow.Snowplow.defaultTracker
@@ -56,6 +58,7 @@ class Demo : Activity(), LoggerDelegate {
private var _startButton: Button? = null
private var _tabButton: Button? = null
private var _loadWebViewButton: Button? = null
+ private var _videoBtn: Button? = null
private var _uriField: EditText? = null
private var _webViewUriField: EditText? = null
private var _type: RadioGroup? = null
@@ -97,6 +100,13 @@ class Demo : Activity(), LoggerDelegate {
_webViewUriField = findViewById(R.id.web_view_uri_field) as EditText
_webView = findViewById(R.id.web_view) as WebView
_loadWebViewButton = findViewById(R.id.btn_load_webview) as Button
+ _videoBtn = findViewById(R.id.btn_lite_video) as Button
+ _videoBtn?.setOnClickListener {
+ Logger.updateLogLevel(LogLevel.VERBOSE)
+ val intent = Intent(this@Demo, MediaActivity::class.java)
+ startActivity(intent)
+ }
+
_logOutput?.movementMethod = ScrollingMovementMethod()
_logOutput?.text = ""
@@ -293,7 +303,7 @@ class Demo : Activity(), LoggerDelegate {
plugin.afterTrack { event: InspectableEvent ->
println("Tracked event with ${event.entities.size} entities")
}
-
+
createTracker(
applicationContext,
namespace,
diff --git a/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/MediaActivity.kt b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/MediaActivity.kt
new file mode 100644
index 000000000..7193dcd89
--- /dev/null
+++ b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/MediaActivity.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplowdemokotlin
+
+import android.app.Activity
+import android.net.Uri
+import android.os.Bundle
+import com.snowplowanalytics.snowplowdemokotlin.media.VideoViewController
+
+class MediaActivity : Activity() {
+ private var videoViewController: VideoViewController? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_media)
+
+ val uri = Uri.parse("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")
+ videoViewController = VideoViewController(activity = this, uri = uri)
+ }
+
+ override fun onDestroy() {
+ videoViewController?.destroy()
+ videoViewController = null
+ super.onDestroy()
+ }
+}
diff --git a/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/media/VideoView.kt b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/media/VideoView.kt
new file mode 100644
index 000000000..cf46490d3
--- /dev/null
+++ b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/media/VideoView.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplowdemokotlin.media
+
+import android.content.Context
+import android.util.AttributeSet
+
+class VideoView : android.widget.VideoView {
+ private var viewController: VideoViewController? = null
+
+ constructor(context: Context?) : super(context)
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+ constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(
+ context,
+ attrs,
+ defStyle
+ )
+
+ fun setVideoPlayer(player: VideoViewController?) {
+ viewController = player
+ }
+
+ override fun start() {
+ super.start()
+ viewController?.onPlay()
+ }
+
+ override fun pause() {
+ super.pause()
+ viewController?.onPause()
+ }
+
+ override fun seekTo(msec: Int) {
+ super.seekTo(msec)
+ viewController?.onSeekStart()
+ }
+}
+
diff --git a/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/media/VideoViewController.kt b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/media/VideoViewController.kt
new file mode 100644
index 000000000..843645f8c
--- /dev/null
+++ b/snowplow-demo-kotlin/src/main/java/com/snowplowanalytics/snowplowdemokotlin/media/VideoViewController.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplowdemokotlin.media
+
+import android.app.Activity
+import android.content.Context
+import android.media.AudioManager
+import android.media.MediaPlayer
+import android.media.MediaPlayer.*
+import android.net.Uri
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Log
+import android.view.View
+import android.widget.MediaController
+import com.snowplowanalytics.snowplow.Snowplow
+import com.snowplowanalytics.snowplow.event.Event
+import com.snowplowanalytics.snowplow.media.controller.MediaTracking
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import com.snowplowanalytics.snowplow.media.event.*
+import com.snowplowanalytics.snowplowdemokotlin.R
+import java.util.*
+
+class VideoViewController(activity: Activity, uri: Uri) {
+ private val videoView: VideoView = activity.findViewById(R.id.videoView) as VideoView
+ private val mediaController: MediaController = MediaController(activity)
+ private var loaded = false
+ private var seeking = false
+ private val audio: AudioManager = activity.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ private var updateThread: UpdateThread? = null
+ private var mediaTracking: MediaTracking? = null
+
+ /**
+ * Converts the volume to percentage.
+ */
+ private val volume: Int
+ get() {
+ val volumeLevel = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
+ val maxVolumeLevel = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
+ return (volumeLevel.toFloat() / maxVolumeLevel * 100).toInt()
+ }
+
+ /**
+ * Constructs the player entity using information from the videoView.
+ */
+ private val player: MediaPlayerEntity
+ get() = MediaPlayerEntity(
+ currentTime = videoView.currentPosition.toDouble() / 1000,
+ duration = videoView.duration.toDouble() / 1000,
+ paused = !videoView.isPlaying || seeking,
+ volume = volume
+ )
+
+ init {
+ // initialize video view
+ videoView.setVideoPlayer(this)
+ mediaController.setMediaPlayer(videoView)
+ videoView.setMediaController(mediaController)
+ videoView.requestFocus()
+
+ // subscribe listeners
+ videoView.setOnPreparedListener { onPrepared(it) }
+ videoView.setOnInfoListener { _, what, _ -> onInfo(what); true }
+ videoView.setOnCompletionListener { onComplete(it) }
+
+ videoView.setVideoURI(uri)
+ }
+
+ fun onPlay() {
+ load()
+ track(MediaPlayEvent())
+ }
+
+ fun onPause() {
+ track(MediaPauseEvent())
+ }
+
+ fun onSeekStart() {
+ load()
+ seeking = true
+ track(MediaSeekStartEvent())
+ }
+
+ private fun onSeekEnd() {
+ seeking = false
+ track(MediaSeekEndEvent())
+ }
+
+ private fun onPrepared(mediaPlayer: MediaPlayer) {
+ mediaController.show(0)
+ mediaPlayer.setOnSeekCompleteListener { onSeekEnd() }
+ }
+
+ private fun onInfo(what: Int) {
+ when (what) {
+ MEDIA_INFO_BUFFERING_START -> track(MediaBufferStartEvent())
+ MEDIA_INFO_BUFFERING_END -> track(MediaBufferEndEvent())
+ }
+ }
+
+ private fun onComplete(player: MediaPlayer) {
+ mediaController.show(0)
+
+ if (loaded) {
+ track(MediaEndEvent())
+ mediaTracking?.id?.let { Snowplow.defaultTracker?.media?.endMediaTracking(it) }
+ updateThread?.invalidate()
+ reset()
+ }
+ }
+
+ fun destroy() {
+ updateThread?.quit()
+ updateThread = null
+ }
+
+ private fun load() {
+ if (!loaded) {
+ reset()
+ loaded = true
+
+ mediaTracking = Snowplow.defaultTracker?.media?.startMediaTracking(
+ id = UUID.randomUUID().toString(),
+ player = player
+ )
+
+ updateThread = UpdateThread()
+
+ track(MediaReadyEvent())
+ }
+ }
+
+ private fun reset() {
+ loaded = false
+ seeking = false
+ updateThread?.quit()
+ updateThread = null
+ mediaTracking = null
+ }
+
+ private fun track(event: Event) {
+ Log.v(TAG, "Tracking media event: $event")
+ mediaTracking?.track(event, player = player)
+ }
+
+ private inner class UpdateThread : HandlerThread("UpdatePlayerThread") {
+ private var shouldStop = false
+
+ init {
+ start()
+ val handler = Handler(looper)
+ handler.post(object : Runnable {
+ override fun run() {
+ if (shouldStop) { return }
+ mediaTracking?.update(player = player)
+ handler.postDelayed(this, 1000L)
+ }
+ })
+ }
+
+ fun invalidate() {
+ shouldStop = true
+ }
+ }
+
+ companion object {
+ private val TAG = VideoViewController::class.java.simpleName
+ }
+}
+
diff --git a/snowplow-demo-kotlin/src/main/res/layout/activity_demo.xml b/snowplow-demo-kotlin/src/main/res/layout/activity_demo.xml
index b2368dafd..f551651dc 100644
--- a/snowplow-demo-kotlin/src/main/res/layout/activity_demo.xml
+++ b/snowplow-demo-kotlin/src/main/res/layout/activity_demo.xml
@@ -383,6 +383,16 @@
android:text="@string/tab"
android:textSize="16sp"
android:textStyle="bold" />
+
+
diff --git a/snowplow-demo-kotlin/src/main/res/layout/activity_media.xml b/snowplow-demo-kotlin/src/main/res/layout/activity_media.xml
new file mode 100644
index 000000000..45bf5b4d9
--- /dev/null
+++ b/snowplow-demo-kotlin/src/main/res/layout/activity_media.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/snowplow-demo-kotlin/src/main/res/values/strings.xml b/snowplow-demo-kotlin/src/main/res/values/strings.xml
index e989e81c0..2633378da 100644
--- a/snowplow-demo-kotlin/src/main/res/values/strings.xml
+++ b/snowplow-demo-kotlin/src/main/res/values/strings.xml
@@ -41,8 +41,10 @@
Enter collector URI or remote config URIā¦Start!Chrome Tab
+ VideoRunning. Running . Running .
+ Media playback
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallEventTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallEventTest.kt
index 1f76e686f..c0eaa9ec4 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallEventTest.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/ApplicationInstallEventTest.kt
@@ -47,7 +47,7 @@ class ApplicationInstallEventTest {
.installAutotracking(true)
createTracker(listOf(trackerConfiguration, plugin))
- Thread.sleep(500)
+ Thread.sleep(1000)
// check if event was tracked
Assert.assertTrue(eventTracked)
@@ -65,7 +65,7 @@ class ApplicationInstallEventTest {
.installAutotracking(true)
createTracker(listOf(trackerConfiguration, plugin))
- Thread.sleep(500)
+ Thread.sleep(1000)
// check if event was tracked
Assert.assertTrue(eventTracked)
@@ -76,7 +76,7 @@ class ApplicationInstallEventTest {
// create tracker again
createTracker(listOf(trackerConfiguration, plugin))
- Thread.sleep(500)
+ Thread.sleep(1000)
// check if event was tracked
Assert.assertFalse(eventTracked)
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/DeepLinkReceivedTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/DeepLinkReceivedTest.kt
index 163e476d8..2c9a87ca4 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/DeepLinkReceivedTest.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/event/DeepLinkReceivedTest.kt
@@ -54,8 +54,8 @@ class DeepLinkReceivedTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
// Prepare DeepLinkReceived event
- val event = DeepLinkReceived("url")
- .referrer("referrer")
+ val event = DeepLinkReceived("someappwithaverylongscheme://url")
+ .referrer("someappwithaverylongscheme://referrer")
// Setup tracker
val trackerConfiguration = TrackerConfiguration("appId")
@@ -89,8 +89,8 @@ class DeepLinkReceivedTest {
// Check url and referrer fields for atomic table
val url = payload.map[Parameters.PAGE_URL] as String?
val referrer = payload.map[Parameters.PAGE_REFR] as String?
- Assert.assertEquals("url", url)
- Assert.assertEquals("referrer", referrer)
+ Assert.assertEquals("someappwithavery://url", url)
+ Assert.assertEquals("someappwithavery://referrer", referrer)
}
@Test
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/remoteconfiguration/RemoteConfigurationTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/remoteconfiguration/RemoteConfigurationTest.kt
index 431e9b3a2..a1557518e 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/remoteconfiguration/RemoteConfigurationTest.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/remoteconfiguration/RemoteConfigurationTest.kt
@@ -16,14 +16,11 @@ import android.annotation.SuppressLint
import androidx.core.util.Pair
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
-import com.snowplowanalytics.core.remoteconfiguration.ConfigurationCache
-import com.snowplowanalytics.core.remoteconfiguration.ConfigurationFetcher
-import com.snowplowanalytics.core.remoteconfiguration.ConfigurationProvider
-import com.snowplowanalytics.core.remoteconfiguration.FetchedConfigurationBundle
-import com.snowplowanalytics.snowplow.configuration.ConfigurationBundle
-import com.snowplowanalytics.snowplow.configuration.ConfigurationState
-import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration
-import com.snowplowanalytics.snowplow.configuration.RemoteConfiguration
+import com.snowplowanalytics.core.remoteconfiguration.RemoteConfigurationCache
+import com.snowplowanalytics.core.remoteconfiguration.RemoteConfigurationFetcher
+import com.snowplowanalytics.core.remoteconfiguration.RemoteConfigurationProvider
+import com.snowplowanalytics.core.remoteconfiguration.RemoteConfigurationBundle
+import com.snowplowanalytics.snowplow.configuration.*
import com.snowplowanalytics.snowplow.network.HttpMethod
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
@@ -48,23 +45,24 @@ class RemoteConfigurationTest {
+ "{\"namespace\": \"default1\","
+ "\"networkConfiguration\": {\"endpoint\":\"https://fake.snowplow.io\",\"method\":\"get\"},"
+ "\"trackerConfiguration\": {\"applicationContext\":false,\"screenContext\":false},"
- + "\"sessionConfiguration\": {\"backgroundTimeout\":60,\"foregroundTimeout\":60}"
+ + "\"sessionConfiguration\": {\"backgroundTimeout\":60,\"foregroundTimeout\":60},"
+ + "\"emitterConfiguration\": {\"serverAnonymisation\":true,\"customRetryForStatusCodes\":{\"500\":true}}"
+ "},"
+ "{\"namespace\": \"default2\","
+ "\"subjectConfiguration\": {\"userId\":\"testUserId\"}"
+ "}"
+ "]}")
val json = JSONObject(config)
- val fetchedConfigurationBundle = FetchedConfigurationBundle(context, json)
+ val remoteConfigurationBundle = RemoteConfigurationBundle(context, json)
Assert.assertEquals(
"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0",
- fetchedConfigurationBundle.schema
+ remoteConfigurationBundle.schema
)
- Assert.assertEquals(12, fetchedConfigurationBundle.configurationVersion.toLong())
- Assert.assertEquals(2, fetchedConfigurationBundle.configurationBundle.size.toLong())
+ Assert.assertEquals(12, remoteConfigurationBundle.configurationVersion.toLong())
+ Assert.assertEquals(2, remoteConfigurationBundle.configurationBundle.size.toLong())
// Regular setup
- var configurationBundle = fetchedConfigurationBundle.configurationBundle[0]
+ var configurationBundle = remoteConfigurationBundle.configurationBundle[0]
Assert.assertEquals("default1", configurationBundle.namespace)
Assert.assertNotNull(configurationBundle.networkConfiguration)
Assert.assertNotNull(configurationBundle.trackerConfiguration)
@@ -76,9 +74,12 @@ class RemoteConfigurationTest {
Assert.assertFalse(trackerConfiguration!!.applicationContext)
val sessionConfiguration = configurationBundle.sessionConfiguration
Assert.assertEquals(60, sessionConfiguration!!.foregroundTimeout.convert(TimeUnit.SECONDS))
+ val emitterConfiguration = configurationBundle.emitterConfiguration
+ Assert.assertTrue(emitterConfiguration!!.serverAnonymisation)
+ Assert.assertEquals(mapOf(500 to true), emitterConfiguration.customRetryForStatusCodes)
// Regular setup without NetworkConfiguration
- configurationBundle = fetchedConfigurationBundle.configurationBundle[1]
+ configurationBundle = remoteConfigurationBundle.configurationBundle[1]
Assert.assertEquals("default2", configurationBundle.namespace)
Assert.assertNull(configurationBundle.networkConfiguration)
Assert.assertNotNull(configurationBundle.subjectConfiguration)
@@ -96,14 +97,14 @@ class RemoteConfigurationTest {
val expectation = Any() as Object
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- ConfigurationFetcher(
+ RemoteConfigurationFetcher(
context,
remoteConfig
- ) { fetchedConfigurationBundle: FetchedConfigurationBundle ->
- Assert.assertNotNull(fetchedConfigurationBundle)
+ ) { remoteConfigurationBundle: RemoteConfigurationBundle ->
+ Assert.assertNotNull(remoteConfigurationBundle)
Assert.assertEquals(
"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0",
- fetchedConfigurationBundle.schema
+ remoteConfigurationBundle.schema
)
synchronized(expectation) { expectation.notify() }
}
@@ -117,14 +118,14 @@ class RemoteConfigurationTest {
val bundle = ConfigurationBundle("test")
bundle.networkConfiguration = NetworkConfiguration("endpoint")
val expected =
- FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
+ RemoteConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
expected.configurationVersion = 12
expected.configurationBundle = listOf(bundle)
val remoteConfiguration = RemoteConfiguration("http://example.com", HttpMethod.GET)
- var cache = ConfigurationCache(remoteConfiguration)
+ var cache = RemoteConfigurationCache(remoteConfiguration)
cache.clearCache(context)
cache.writeCache(context, expected)
- cache = ConfigurationCache(remoteConfiguration)
+ cache = RemoteConfigurationCache(remoteConfiguration)
val config = cache.readCache(context)
Assert.assertEquals(
expected.configurationVersion.toLong(),
@@ -144,6 +145,30 @@ class RemoteConfigurationTest {
Assert.assertNull(configBundle.trackerConfiguration)
}
+ @Test
+ fun testCacheEmitterConfiguration() {
+ val context = InstrumentationRegistry.getInstrumentation().targetContext
+ val bundle = ConfigurationBundle("test", NetworkConfiguration("endpoint"))
+ bundle.emitterConfiguration = EmitterConfiguration()
+ .serverAnonymisation(true)
+ .customRetryForStatusCodes(mapOf(500 to true))
+ val expected =
+ RemoteConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
+ expected.configurationVersion = 12
+ expected.configurationBundle = listOf(bundle)
+
+ val remoteConfiguration = RemoteConfiguration("http://example.com", HttpMethod.GET)
+ var cache = RemoteConfigurationCache(remoteConfiguration)
+ cache.clearCache(context)
+ cache.writeCache(context, expected)
+ cache = RemoteConfigurationCache(remoteConfiguration)
+ val config = cache.readCache(context)
+ val emitterConfig = config?.configurationBundle?.first()?.emitterConfiguration
+
+ Assert.assertTrue(emitterConfig?.serverAnonymisation ?: false)
+ Assert.assertEquals(mapOf(500 to true), emitterConfig?.customRetryForStatusCodes)
+ }
+
@Test
@Throws(IOException::class, InterruptedException::class)
fun testConfigurationFetcher_downloads() {
@@ -158,7 +183,7 @@ class RemoteConfigurationTest {
val expectation = Any() as Object
val expectationNotified = AtomicBoolean(false)
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- ConfigurationFetcher(
+ RemoteConfigurationFetcher(
context,
remoteConfig
) {
@@ -177,11 +202,11 @@ class RemoteConfigurationTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
withMockServer(500, "{}") { _, endpoint ->
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- val cache = ConfigurationCache(remoteConfig)
+ val cache = RemoteConfigurationCache(remoteConfig)
cache.clearCache(context)
// test
- val provider = ConfigurationProvider(remoteConfig)
+ val provider = RemoteConfigurationProvider(remoteConfig)
provider.retrieveConfiguration(context, false) { Assert.fail() }
Thread.sleep(1000)
}
@@ -197,11 +222,11 @@ class RemoteConfigurationTest {
"{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0\",\"configurationVersion\":12,\"configurationBundle\":[]}"
) { _, endpoint ->
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- val cache = ConfigurationCache(remoteConfig)
+ val cache = RemoteConfigurationCache(remoteConfig)
cache.clearCache(context)
// test
- val provider = ConfigurationProvider(remoteConfig)
+ val provider = RemoteConfigurationProvider(remoteConfig)
provider.retrieveConfiguration(context, false) { Assert.fail() }
Thread.sleep(1000)
}
@@ -217,23 +242,23 @@ class RemoteConfigurationTest {
"{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":1,\"configurationBundle\":[]}"
) { _, endpoint ->
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- val cache = ConfigurationCache(remoteConfig)
+ val cache = RemoteConfigurationCache(remoteConfig)
cache.clearCache(context)
val bundle = ConfigurationBundle("namespace")
bundle.networkConfiguration = NetworkConfiguration("endpoint")
val cached =
- FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
+ RemoteConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
cached.configurationVersion = 1
cached.configurationBundle = listOf(bundle)
cache.writeCache(context, cached)
// test
- val provider = ConfigurationProvider(remoteConfig)
+ val provider = RemoteConfigurationProvider(remoteConfig)
var numCalls = 0
provider.retrieveConfiguration(
context,
false
- ) { pair: Pair ->
+ ) { pair: Pair ->
val fetchedConfigurationBundle = pair.first
Assert.assertEquals(ConfigurationState.CACHED, pair.second)
if (numCalls == 1 || fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0") {
@@ -258,23 +283,23 @@ class RemoteConfigurationTest {
"{\"\$schema\":\"http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0\",\"configurationVersion\":2,\"configurationBundle\":[]}"
) { _, endpoint ->
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- val cache = ConfigurationCache(remoteConfig)
+ val cache = RemoteConfigurationCache(remoteConfig)
cache.clearCache(context)
val bundle = ConfigurationBundle("namespace")
bundle.networkConfiguration = NetworkConfiguration("endpoint")
val cached =
- FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
+ RemoteConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
cached.configurationVersion = 1
cached.configurationBundle = listOf(bundle)
cache.writeCache(context, cached)
// test
- val provider = ConfigurationProvider(remoteConfig)
+ val provider = RemoteConfigurationProvider(remoteConfig)
var numCalls = 0
provider.retrieveConfiguration(
context,
false
- ) { pair: Pair ->
+ ) { pair: Pair ->
val fetchedConfigurationBundle = pair.first
Assert.assertEquals(
if (numCalls == 0) ConfigurationState.CACHED else ConfigurationState.FETCHED,
@@ -299,21 +324,21 @@ class RemoteConfigurationTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
withMockServer(404, "{}") { mockWebServer, endpoint ->
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- val cache = ConfigurationCache(remoteConfig)
+ val cache = RemoteConfigurationCache(remoteConfig)
cache.clearCache(context)
val bundle = ConfigurationBundle("namespace")
bundle.networkConfiguration = NetworkConfiguration("endpoint")
val cached =
- FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
+ RemoteConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
cached.configurationVersion = 1
cached.configurationBundle = listOf(bundle)
cache.writeCache(context, cached)
val expectation = Any() as Object
- val provider = ConfigurationProvider(remoteConfig)
+ val provider = RemoteConfigurationProvider(remoteConfig)
provider.retrieveConfiguration(
context,
false
- ) { pair: Pair ->
+ ) { pair: Pair ->
Assert.assertEquals(ConfigurationState.CACHED, pair.second)
synchronized(expectation) { expectation.notify() }
}
@@ -337,17 +362,17 @@ class RemoteConfigurationTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
withMockServer(404, "{}") { mockWebServer, endpoint ->
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- val cache = ConfigurationCache(remoteConfig)
+ val cache = RemoteConfigurationCache(remoteConfig)
cache.clearCache(context)
val bundle = ConfigurationBundle("namespace")
bundle.networkConfiguration = NetworkConfiguration("endpoint")
val cached =
- FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
+ RemoteConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
cached.configurationVersion = 1
cached.configurationBundle = listOf(bundle)
cache.writeCache(context, cached)
val expectation = Any() as Object
- val provider = ConfigurationProvider(remoteConfig)
+ val provider = RemoteConfigurationProvider(remoteConfig)
provider.retrieveConfiguration(
context,
false
@@ -367,7 +392,7 @@ class RemoteConfigurationTest {
provider.retrieveConfiguration(
context,
true
- ) { pair: Pair ->
+ ) { pair: Pair ->
val fetchedConfigurationBundle = pair.first
if (fetchedConfigurationBundle.schema == "http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-1-0") {
numCallbackCalls++
@@ -386,14 +411,14 @@ class RemoteConfigurationTest {
// prepare test
val context = InstrumentationRegistry.getInstrumentation().targetContext
val cachedRemoteConfig = RemoteConfiguration("http://cache.example.com", HttpMethod.GET)
- val cache = ConfigurationCache(cachedRemoteConfig)
+ val cache = RemoteConfigurationCache(cachedRemoteConfig)
cache.clearCache(context)
// write configuration (version 2) to cache
val bundle = ConfigurationBundle("namespace")
bundle.networkConfiguration = NetworkConfiguration("endpoint")
val cached =
- FetchedConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
+ RemoteConfigurationBundle("http://iglucentral.com/schemas/com.snowplowanalytics.mobile/remote_config/jsonschema/1-0-0")
cached.configurationVersion = 2
cached.configurationBundle = listOf(bundle)
cache.writeCache(context, cached)
@@ -406,12 +431,12 @@ class RemoteConfigurationTest {
// retrieve remote configuration
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- val provider = ConfigurationProvider(remoteConfig)
+ val provider = RemoteConfigurationProvider(remoteConfig)
var numCallbackCalls = 0
provider.retrieveConfiguration(
context,
true
- ) { pair: Pair ->
+ ) { pair: Pair ->
val fetchedConfigurationBundle = pair.first
numCallbackCalls++
// should be the non-cache configuration (version 1)
@@ -431,7 +456,7 @@ class RemoteConfigurationTest {
// prepare test
val context = InstrumentationRegistry.getInstrumentation().targetContext
val cachedRemoteConfig = RemoteConfiguration("http://cache.example.com", HttpMethod.GET)
- ConfigurationCache(cachedRemoteConfig).clearCache(context)
+ RemoteConfigurationCache(cachedRemoteConfig).clearCache(context)
// stub request for configuration (return version 1)
withMockServer(
@@ -441,7 +466,7 @@ class RemoteConfigurationTest {
// retrieve remote configuration
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- val provider = ConfigurationProvider(
+ val provider = RemoteConfigurationProvider(
remoteConfiguration = remoteConfig,
defaultBundles = listOf(
ConfigurationBundle("namespace", NetworkConfiguration("http://localhost"))
@@ -452,7 +477,7 @@ class RemoteConfigurationTest {
provider.retrieveConfiguration(
context,
false
- ) { pair: Pair ->
+ ) { pair: Pair ->
numCallbackCalls++
Assert.assertEquals(ConfigurationState.DEFAULT, pair.second)
}
@@ -466,7 +491,7 @@ class RemoteConfigurationTest {
// prepare test
val context = InstrumentationRegistry.getInstrumentation().targetContext
val cachedRemoteConfig = RemoteConfiguration("http://cache.example.com", HttpMethod.GET)
- ConfigurationCache(cachedRemoteConfig).clearCache(context)
+ RemoteConfigurationCache(cachedRemoteConfig).clearCache(context)
// stub request for configuration (return version 2)
withMockServer(
@@ -476,7 +501,7 @@ class RemoteConfigurationTest {
// retrieve remote configuration
val remoteConfig = RemoteConfiguration(endpoint, HttpMethod.GET)
- val provider = ConfigurationProvider(
+ val provider = RemoteConfigurationProvider(
remoteConfiguration = remoteConfig,
defaultBundles = listOf(
ConfigurationBundle("namespace", NetworkConfiguration("http://localhost"))
@@ -488,7 +513,7 @@ class RemoteConfigurationTest {
provider.retrieveConfiguration(
context,
false
- ) { pair: Pair ->
+ ) { pair: Pair ->
numCallbackCalls++
lastConfigurationState = pair.second
}
@@ -498,6 +523,32 @@ class RemoteConfigurationTest {
}
}
+ @Test
+ fun testKeepsPropertiesOfSourceConfigurationIfNotOverridenInRemote() {
+ val bundle1 = ConfigurationBundle("ns1")
+ bundle1.trackerConfiguration = TrackerConfiguration("app-1")
+ bundle1.subjectConfiguration = SubjectConfiguration()
+ .domainUserId("duid1")
+ .userId("u1")
+
+ val bundle2 = ConfigurationBundle("ns1")
+ bundle2.subjectConfiguration = SubjectConfiguration()
+ .domainUserId("duid2")
+
+ val remoteBundle1 = RemoteConfigurationBundle("")
+ remoteBundle1.configurationBundle = listOf(bundle1)
+
+ val remoteBundle2 = RemoteConfigurationBundle("")
+ remoteBundle2.configurationBundle = listOf(bundle2)
+
+ remoteBundle2.updateSourceConfig(remoteBundle1)
+
+ val finalBundle = remoteBundle2.configurationBundle.first()
+ Assert.assertEquals("app-1", finalBundle.trackerConfiguration?.appId)
+ Assert.assertEquals("u1", finalBundle.subjectConfiguration?.userId)
+ Assert.assertEquals("duid2", finalBundle.subjectConfiguration?.domainUserId)
+ }
+
// Private methods
@Throws(IOException::class)
private fun withMockServer(responseCode: Int, body: String?, callback: (MockWebServer, String) -> Unit) {
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/MockDeviceInfoMonitor.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/MockDeviceInfoMonitor.kt
index e4a3cc9ed..8a55dfc38 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/MockDeviceInfoMonitor.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/MockDeviceInfoMonitor.kt
@@ -87,10 +87,14 @@ class MockDeviceInfoMonitor : DeviceInfoMonitor() {
return 70000
}
- override val language: String?
+ private var _language: String? = "sk"
+ override var language: String?
get() {
increaseMethodAccessCount("language")
- return "sk"
+ return _language
+ }
+ set(value) {
+ _language = value
}
override fun getResolution(context: Context): String? {
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/PlatformContextTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/PlatformContextTest.kt
index 4666ffcf8..5fe1baf3d 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/PlatformContextTest.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/PlatformContextTest.kt
@@ -233,6 +233,19 @@ class PlatformContextTest {
Assert.assertFalse(sdjData.containsKey(Parameters.IS_PORTRAIT))
}
+ @Test
+ fun truncatesLanguageToMax8Chars() {
+ val deviceInfoMonitor = MockDeviceInfoMonitor()
+ deviceInfoMonitor.language = "1234567890"
+ val platformContext = PlatformContext(0, 0, deviceInfoMonitor, null, context)
+
+ val sdj = platformContext.getMobileContext(false)
+ Assert.assertNotNull(sdj)
+ val sdjData = sdj!!.map["data"] as Map<*, *>
+
+ Assert.assertEquals("12345678", sdjData[Parameters.MOBILE_LANGUAGE])
+ }
+
// --- PRIVATE
private val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/TrackerTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/TrackerTest.kt
index 8bb1d96f7..579015890 100755
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/TrackerTest.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/TrackerTest.kt
@@ -25,7 +25,7 @@ import com.snowplowanalytics.core.tracker.ExceptionHandler
import com.snowplowanalytics.core.tracker.Subject
import com.snowplowanalytics.core.tracker.Tracker
import com.snowplowanalytics.core.tracker.TrackerEvent
-import com.snowplowanalytics.snowplow.TestUtils
+import com.snowplowanalytics.snowplow.util.TestUtils
import com.snowplowanalytics.snowplow.emitter.BufferOption
import com.snowplowanalytics.snowplow.event.ScreenView
import com.snowplowanalytics.snowplow.event.SelfDescribing
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/utils/UtilTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/utils/UtilTest.kt
index 84d9bdd7b..348592d6e 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/utils/UtilTest.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/utils/UtilTest.kt
@@ -22,6 +22,7 @@ import com.snowplowanalytics.core.utils.Util.joinLongList
import com.snowplowanalytics.core.utils.Util.mapHasKeys
import com.snowplowanalytics.core.utils.Util.serialize
import com.snowplowanalytics.core.utils.Util.timestamp
+import com.snowplowanalytics.core.utils.Util.truncateUrlScheme
import com.snowplowanalytics.core.utils.Util.uUIDString
import org.junit.Assert
import org.junit.Test
@@ -120,4 +121,23 @@ class UtilTest {
Assert.assertEquals(1, map.size.toLong())
Assert.assertEquals("world", map["hello"])
}
+
+ @Test
+ fun testTruncateUrlSchemeDoesntChangeValidUrl() {
+ val url = "https://docs.snowplow.io/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol/#snowplow-events"
+ Assert.assertEquals(url, truncateUrlScheme(url))
+ }
+
+ @Test
+ fun testTruncateUrlSchemeDoesntChangeInvalidUrl() {
+ val url = "this is not a valid URL"
+ Assert.assertEquals(url, truncateUrlScheme(url))
+ }
+
+ @Test
+ fun testTruncateUrlSchemeTruncatesLongUrlScheme() {
+ val url = "12345678901234567890://docs.snowplow.io/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol/#snowplow-events"
+ val truncated = "1234567890123456://docs.snowplow.io/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol/#snowplow-events"
+ Assert.assertEquals(truncated, truncateUrlScheme(url))
+ }
}
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/MediaAdTrackingTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/MediaAdTrackingTest.kt
new file mode 100644
index 000000000..086a0f18d
--- /dev/null
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/MediaAdTrackingTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.snowplowanalytics.core.media.controller.MediaAdTracking
+import com.snowplowanalytics.snowplow.media.entity.MediaAdBreakEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaAdEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import com.snowplowanalytics.snowplow.media.event.MediaAdBreakStartEvent
+import com.snowplowanalytics.snowplow.media.event.MediaAdStartEvent
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MediaAdTrackingTest {
+
+ @Test
+ fun updatesStartTimeOfAdBreak() {
+ val adTracking = MediaAdTracking()
+
+ adTracking.updateForThisEvent(
+ event = MediaAdBreakStartEvent(),
+ player = MediaPlayerEntity(currentTime = 33.0),
+ adBreak = MediaAdBreakEntity(breakId = "b1")
+ )
+ adTracking.updateForNextEvent(
+ event = MediaAdBreakStartEvent()
+ )
+
+ adTracking.updateForThisEvent(
+ event = MediaAdStartEvent(),
+ player = MediaPlayerEntity(currentTime = 44.0),
+ ad = MediaAdEntity(adId = "a1")
+ )
+ adTracking.updateForNextEvent(
+ event = MediaAdStartEvent()
+ )
+
+ assertEquals("b1", adTracking.adBreak?.breakId)
+ assertEquals(33.0, adTracking.adBreak?.startTime ?: 0.0, 0.0)
+ }
+
+ @Test
+ fun updatesPodPositionOfAds() {
+ val adTracking = MediaAdTracking()
+
+ adTracking.updateForThisEvent(
+ event = MediaAdBreakStartEvent(),
+ player = MediaPlayerEntity(),
+ adBreak = MediaAdBreakEntity(breakId = "b1")
+ )
+ adTracking.updateForNextEvent(
+ event = MediaAdBreakStartEvent()
+ )
+
+ adTracking.updateForThisEvent(
+ event = MediaAdStartEvent(),
+ player = MediaPlayerEntity(),
+ ad = MediaAdEntity(adId = "a1")
+ )
+
+ assertEquals(1, adTracking.ad?.podPosition)
+
+ adTracking.updateForNextEvent(
+ event = MediaAdStartEvent()
+ )
+
+ adTracking.updateForThisEvent(
+ event = MediaAdStartEvent(),
+ player = MediaPlayerEntity(),
+ ad = MediaAdEntity(adId = "a2")
+ )
+
+ assertEquals(2, adTracking.ad?.podPosition)
+ }
+}
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/MediaEventAndEntitySerializationTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/MediaEventAndEntitySerializationTest.kt
new file mode 100644
index 000000000..8005918a7
--- /dev/null
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/MediaEventAndEntitySerializationTest.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.snowplowanalytics.core.media.controller.MediaSessionTrackingStats
+import com.snowplowanalytics.core.media.entity.MediaSessionEntity
+import com.snowplowanalytics.core.utils.Util.getDateTimeFromDate
+import com.snowplowanalytics.snowplow.media.entity.*
+import com.snowplowanalytics.snowplow.media.event.*
+import com.snowplowanalytics.snowplow.util.TimeTraveler
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.Date
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+@RunWith(AndroidJUnit4::class)
+class MediaEventAndEntitySerializationTest {
+
+ @Test
+ fun schemaForMediaEventTypes() {
+ assertEquals(mediaSchema("play_event"), MediaPlayEvent().schema)
+ assertEquals(mediaSchema("playback_rate_change_event"), MediaPlaybackRateChangeEvent(newRate = 1.0).schema)
+ assertEquals(mediaSchema("ready_event"), MediaReadyEvent().schema)
+ assertEquals(mediaSchema("ad_resume_event"), MediaAdResumeEvent().schema)
+
+ assertEquals(mediaSchema("ad_quartile_event"), MediaAdFirstQuartileEvent().schema)
+ assertEquals(mediaSchema("ad_quartile_event"), MediaAdMidpointEvent().schema)
+ assertEquals(mediaSchema("ad_quartile_event"), MediaAdThirdQuartileEvent().schema)
+ assertEquals(mediaSchema("ad_complete_event"), MediaAdCompleteEvent().schema)
+ }
+
+ @Test
+ fun buildsEntityWithDefaultValuesForEmptyMediaPlayer() {
+ val entity = MediaPlayerEntity().entity
+
+ assertEquals(mediaSchema("player"), entity.map["schema"] as? String)
+ assertEquals(0.0, (entity.map["data"] as? Map<*, *>)?.get("currentTime"))
+ assertEquals(true, (entity.map["data"] as? Map<*, *>)?.get("paused"))
+ assertEquals(false, (entity.map["data"] as? Map<*, *>)?.get("ended"))
+ }
+
+ @Test
+ fun buildsEntityForMediaPlayer() {
+ val entity = MediaPlayerEntity(
+ currentTime = 33.3,
+ duration = 100.0,
+ ended = true,
+ fullscreen = true,
+ livestream = true,
+ label = "The Video",
+ loop = true,
+ mediaType = MediaType.Video,
+ muted = true,
+ paused = false,
+ pictureInPicture = false,
+ playerType = "AVPlayer",
+ playbackRate = 2.5,
+ quality = "1080p",
+ volume = 80,
+ )
+
+ assertEquals(mediaSchema("player"), entity.entity.map["schema"] as? String)
+ val data = entity.entity.map["data"] as? Map<*, *>
+ assertEquals(33.3, data?.get("currentTime"))
+ assertEquals(100.0, data?.get("duration"))
+ assertEquals(true, data?.get("ended"))
+ assertEquals(true, data?.get("fullscreen"))
+ assertEquals(true, data?.get("livestream"))
+ assertEquals("The Video", data?.get("label"))
+ assertEquals(true, data?.get("loop"))
+ assertEquals("video", data?.get("mediaType"))
+ assertEquals(true, data?.get("muted"))
+ assertEquals(false, data?.get("paused"))
+ assertEquals(false, data?.get("pictureInPicture"))
+ assertEquals("AVPlayer", data?.get("playerType"))
+ assertEquals(2.5, data?.get("playbackRate"))
+ assertEquals("1080p", data?.get("quality"))
+ assertEquals(80, data?.get("volume"))
+ }
+
+ @Test
+ fun buildsMediaSessionEntity() {
+ val date = Date()
+ val timeTraveler = TimeTraveler()
+ val session = MediaSessionEntity(id = "xxx", startedAt = date, pingInterval = 13)
+ val stats = MediaSessionTrackingStats(
+ session = session,
+ dateGenerator = { timeTraveler.generateDate() },
+ )
+
+ stats.update(
+ event = MediaPlayEvent(),
+ player = MediaPlayerEntity(currentTime = 0.0, paused = false),
+ )
+ timeTraveler.travelBy(10.toDuration(DurationUnit.SECONDS))
+ stats.update(
+ event = MediaPauseEvent(),
+ player = MediaPlayerEntity(currentTime = 10.0, paused = true),
+ )
+
+ val entity = session.entity(stats)
+ assertEquals(mediaSchema("session"), entity.map["schema"] as? String)
+ val data = entity.map["data"] as? Map<*, *>
+ assertEquals("xxx", data?.get("mediaSessionId"))
+ assertEquals(getDateTimeFromDate(date), data?.get("startedAt"))
+ assertEquals(13, data?.get("pingInterval"))
+ assertEquals(10.0, data?.get("timePlayed"))
+ assertFalse(data?.containsKey("timePaused") ?: true)
+ }
+
+ @Test
+ fun buildsAdEntity() {
+ val ad = MediaAdEntity(
+ name = "Name",
+ adId = "yyy",
+ creativeId = "zzz",
+ duration = 11.0,
+ podPosition = 2,
+ skippable = true
+ )
+ val entity = ad.entity
+
+ assertEquals(mediaSchema("ad"), entity.map["schema"] as? String)
+ val data = entity.map["data"] as? Map<*, *>
+ assertEquals("Name", data?.get("name"))
+ assertEquals("yyy", data?.get("adId"))
+ assertEquals("zzz", data?.get("creativeId"))
+ assertEquals(11.0, data?.get("duration"))
+ assertEquals(2, data?.get("podPosition"))
+ assertEquals(true, data?.get("skippable"))
+ }
+
+ @Test
+ fun buildsAdBreakEntity() {
+ val adBreak = MediaAdBreakEntity(
+ breakId = "xxx",
+ name = "Break 1",
+ breakType = MediaAdBreakType.NonLinear,
+ podSize = 3
+ )
+ adBreak.startTime = 100.1
+ val entity = adBreak.entity
+
+ assertEquals(mediaSchema("ad_break"), entity.map["schema"] as? String)
+ val data = entity.map["data"] as? Map<*, *>
+ assertEquals("xxx", data?.get("breakId"))
+ assertEquals("Break 1", data?.get("name"))
+ assertEquals("nonlinear", data?.get("breakType"))
+ assertEquals(3, data?.get("podSize"))
+ assertEquals(100.1, data?.get("startTime"))
+ }
+
+ private fun mediaSchema(name: String, version: String = "1-0-0"): String {
+ return "iglu:com.snowplowanalytics.snowplow.media/" + name + "/jsonschema/" + version
+ }
+}
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/MediaSessionTrackingStatsTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/MediaSessionTrackingStatsTest.kt
new file mode 100644
index 000000000..b9fa8c7c6
--- /dev/null
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/MediaSessionTrackingStatsTest.kt
@@ -0,0 +1,280 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.snowplowanalytics.core.media.controller.MediaSessionTrackingStats
+import com.snowplowanalytics.core.media.entity.MediaSessionEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaAdBreakEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaAdBreakType
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import com.snowplowanalytics.snowplow.media.event.*
+import com.snowplowanalytics.snowplow.util.TimeTraveler
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.time.Duration
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+@RunWith(AndroidJUnit4::class)
+class MediaSessionTrackingStatsTest {
+
+ @Test
+ fun calculatesPlayedDuration() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+
+ stats.update(event = MediaPlayEvent(), player = player)
+
+ timeTraveler.travelBy(60.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 60.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(61.toDuration(DurationUnit.SECONDS), stats.contentWatched)
+ assertEquals(60.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ assertEquals(Duration.ZERO, stats.timePlayedMuted)
+ assertEquals(Duration.ZERO, stats.timePaused)
+ assertEquals(1.0, stats.avgPlaybackRate, 0.0)
+ }
+
+ @Test
+ fun considersPauses() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+
+ stats.update(event = MediaPlayEvent(), player = player)
+
+ timeTraveler.travelBy(10.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 10.0
+ stats.update(event = null, player = player)
+ player.paused = true
+ stats.update(event = MediaPauseEvent(), player = player)
+
+ timeTraveler.travelBy(10.toDuration(DurationUnit.SECONDS))
+ player.paused = false
+ stats.update(event = MediaPlayEvent(), player = player)
+
+ timeTraveler.travelBy(50.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 60.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(61.toDuration(DurationUnit.SECONDS), stats.contentWatched)
+ assertEquals(60.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ assertEquals(0.toDuration(DurationUnit.SECONDS), stats.timePlayedMuted)
+ assertEquals(10.toDuration(DurationUnit.SECONDS), stats.timePaused)
+ assertEquals(1.0, stats.avgPlaybackRate, 0.0)
+ }
+
+ @Test
+ fun calculatesPlayOnMute() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+
+ stats.update(event = MediaPlayEvent(), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 30.0
+ player.muted = true
+ stats.update(event = MediaVolumeChangeEvent(newVolume = 50), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 60.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(61.toDuration(DurationUnit.SECONDS), stats.contentWatched)
+ assertEquals(60.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ assertEquals(30.toDuration(DurationUnit.SECONDS), stats.timePlayedMuted)
+ assertEquals(0.toDuration(DurationUnit.SECONDS), stats.timePaused)
+ assertEquals(1.0, stats.avgPlaybackRate, 0.0)
+ }
+
+ @Test
+ fun calculatesAveragePlaybackRate() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+
+ stats.update(event = MediaPlayEvent(), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 30.0
+ player.playbackRate = 2.0
+ stats.update(event = MediaPlaybackRateChangeEvent(newRate = 2.0), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 90.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(91.toDuration(DurationUnit.SECONDS), stats.contentWatched)
+ assertEquals(60.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ assertEquals(0.toDuration(DurationUnit.SECONDS), stats.timePlayedMuted)
+ assertEquals(0.toDuration(DurationUnit.SECONDS), stats.timePaused)
+ assertEquals(1.5, stats.avgPlaybackRate, 0.0)
+ }
+
+ @Test
+ fun calculatesStatsForLinearAds() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+
+ stats.update(event = MediaPlayEvent(), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 30.0
+ stats.update(event = MediaAdStartEvent(), player = player)
+
+ timeTraveler.travelBy(5.toDuration(DurationUnit.SECONDS))
+ stats.update(event = MediaAdClickEvent(), player = player)
+
+ timeTraveler.travelBy(10.toDuration(DurationUnit.SECONDS))
+ stats.update(event = MediaAdCompleteEvent(), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 60.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(15.toDuration(DurationUnit.SECONDS), stats.timeSpentAds)
+ assertEquals(1, stats.ads)
+ assertEquals(1, stats.adsClicked)
+ assertEquals(0, stats.adBreaks)
+ assertEquals(61.toDuration(DurationUnit.SECONDS), stats.contentWatched)
+ assertEquals(60.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ assertEquals(0.toDuration(DurationUnit.SECONDS), stats.timePlayedMuted)
+ assertEquals(0.toDuration(DurationUnit.SECONDS), stats.timePaused)
+ assertEquals(1.0, stats.avgPlaybackRate, 0.0)
+ }
+
+ @Test
+ fun calculatesStatsForNonLinearAds() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+ val adBreak = MediaAdBreakEntity(breakId = "1", breakType = MediaAdBreakType.NonLinear)
+
+ stats.update(event = MediaPlayEvent(), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 30.0
+ stats.update(event = MediaAdBreakStartEvent(), player = player, adBreak = adBreak)
+ stats.update(event = MediaAdStartEvent(), player = player, adBreak = adBreak)
+
+ timeTraveler.travelBy(15.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 45.0
+ stats.update(event = MediaAdCompleteEvent(), player = player, adBreak = adBreak)
+ stats.update(event = MediaAdBreakEndEvent(), player = player, adBreak = adBreak)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 75.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(15.toDuration(DurationUnit.SECONDS), stats.timeSpentAds)
+ assertEquals(1, stats.ads)
+ assertEquals(1, stats.adBreaks)
+ assertEquals(76.toDuration(DurationUnit.SECONDS), stats.contentWatched)
+ assertEquals(75.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ }
+
+ @Test
+ fun countsRewatchedContentOnceInContentWatched() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+
+ stats.update(event = MediaPlayEvent(), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 30.0
+ stats.update(event = MediaSeekStartEvent(), player = player)
+ player.currentTime = 15.0
+ stats.update(event = MediaSeekEndEvent(), player = player)
+
+ timeTraveler.travelBy(45.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 60.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(61.toDuration(DurationUnit.SECONDS), stats.contentWatched)
+ assertEquals(75.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ }
+
+ @Test
+ fun considersChangesInPingEvents() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+
+ stats.update(event = MediaPlayEvent(), player = player)
+
+ for (i in 1 until 60) {
+ timeTraveler.travelBy(1.toDuration(DurationUnit.SECONDS))
+ player.currentTime = (player.currentTime ?: 0.0) + 1.0
+ player.muted = i % 2 == 1
+ stats.update(event = null, player = player)
+ }
+
+ timeTraveler.travelBy(1.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 60.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(61.toDuration(DurationUnit.SECONDS), stats.contentWatched)
+ assertEquals(60.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ assertEquals(30.toDuration(DurationUnit.SECONDS), stats.timePlayedMuted)
+ }
+
+ @Test
+ fun calculatesBufferingTime() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+
+ stats.update(event = MediaBufferStartEvent(), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ stats.update(event = MediaBufferEndEvent(), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 30.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(60.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ assertEquals(30.toDuration(DurationUnit.SECONDS), stats.timeBuffering)
+ }
+
+ @Test
+ fun endsBufferingWhenPlaybackTimeMoves() {
+ val (stats, timeTraveler) = createStatsAndTraveler()
+ val player = MediaPlayerEntity(paused = false)
+
+ stats.update(event = MediaBufferStartEvent(), player = player)
+
+ timeTraveler.travelBy(30.toDuration(DurationUnit.SECONDS))
+ stats.update(event = null, player = player)
+
+ timeTraveler.travelBy(1.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 1.0
+ stats.update(event = null, player = player)
+
+ timeTraveler.travelBy(29.toDuration(DurationUnit.SECONDS))
+ player.currentTime = 30.0
+ stats.update(event = MediaEndEvent(), player = player)
+
+ assertEquals(60.toDuration(DurationUnit.SECONDS), stats.timePlayed)
+ assertEquals(31.toDuration(DurationUnit.SECONDS), stats.timeBuffering)
+ }
+
+ private fun createStatsAndTraveler(): Pair {
+ val timeTraveler = TimeTraveler()
+ val session = MediaSessionEntity(id = "1")
+ val stats = MediaSessionTrackingStats(
+ session = session,
+ dateGenerator = { timeTraveler.generateDate() }
+ )
+ return Pair(stats, timeTraveler)
+ }
+}
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/TestMediaController.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/TestMediaController.kt
new file mode 100644
index 000000000..bb3e399b4
--- /dev/null
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/media/TestMediaController.kt
@@ -0,0 +1,624 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.snowplowanalytics.core.media.MediaSchemata.eventSchema
+import com.snowplowanalytics.core.media.MediaSchemata.playerSchema
+import com.snowplowanalytics.core.media.MediaSchemata.sessionSchema
+import com.snowplowanalytics.core.media.controller.MediaPingInterval
+import com.snowplowanalytics.core.media.controller.MediaSessionTracking
+import com.snowplowanalytics.core.media.controller.MediaTrackingImpl
+import com.snowplowanalytics.core.media.controller.TimerInterface
+import com.snowplowanalytics.snowplow.Snowplow.createTracker
+import com.snowplowanalytics.snowplow.Snowplow.removeAllTrackers
+import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration
+import com.snowplowanalytics.snowplow.configuration.PluginConfiguration
+import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration
+import com.snowplowanalytics.snowplow.controller.TrackerController
+import com.snowplowanalytics.snowplow.event.SelfDescribing
+import com.snowplowanalytics.snowplow.media.configuration.MediaTrackingConfiguration
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import com.snowplowanalytics.snowplow.media.event.*
+import com.snowplowanalytics.snowplow.network.HttpMethod
+import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
+import com.snowplowanalytics.snowplow.tracker.InspectableEvent
+import com.snowplowanalytics.snowplow.tracker.MockNetworkConnection
+import com.snowplowanalytics.snowplow.util.TimeTraveler
+import org.junit.After
+import org.junit.Assert.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.*
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+@RunWith(AndroidJUnit4::class)
+class TestMediaController {
+
+ private val trackedEvents: MutableList = mutableListOf()
+ private val firstEvent: InspectableEvent
+ get() = trackedEvents.first()
+ private val firstPlayer: Map<*, *>?
+ get() = firstEvent.entities.find { it.map["schema"] == playerSchema }?.map?.get("data") as? Map<*, *>
+ private val secondPlayer: Map<*, *>?
+ get() = trackedEvents[1].entities.find { it.map["schema"] == playerSchema }?.map?.get("data") as? Map<*, *>
+ private val firstSession: Map<*, *>?
+ get() = firstEvent.entities.find { it.map["schema"] == sessionSchema }?.map?.get("data") as? Map<*, *>
+ private var tracker: TrackerController? = null
+
+ @Before
+ fun setUp() {
+ tracker = createTracker()
+ }
+
+ @After
+ fun tearDown() {
+ tracker?.media?.endMediaTracking("media1")
+ tracker?.pause()
+ tracker = null
+ removeAllTrackers()
+ trackedEvents.clear()
+ }
+
+ // --- MEDIA PLAYER EVENT TESTS
+
+ @Test
+ fun setsPausedToFalseWhenPlayEventIsTracked() {
+ val media = tracker?.media?.startMediaTracking(
+ id = "media1",
+ player = MediaPlayerEntity(paused = true)
+ )
+ media?.track(MediaPlayEvent())
+
+ Thread.sleep(100)
+
+ assertEquals(1, trackedEvents.size)
+ assertEquals(eventSchema("play"), firstEvent.schema)
+ assertFalse(firstPlayer?.get("paused") as Boolean)
+ }
+
+ @Test
+ fun setsPausedAndEndedToTrueWhenEndIsTracked() {
+ val media = tracker?.media?.startMediaTracking(
+ id = "media1",
+ player = MediaPlayerEntity(paused = false)
+ )
+ media?.track(MediaEndEvent())
+
+ Thread.sleep(100)
+
+ assertEquals(1, trackedEvents.size)
+ assertEquals(eventSchema("end"), firstEvent.schema)
+ assertEquals(true, firstPlayer?.get("paused"))
+ assertEquals(true, firstPlayer?.get("ended"))
+ }
+
+ @Test
+ fun testDoesntTrackSeekStartMultipleTimes() {
+ val media = tracker?.media?.startMediaTracking(id = "media1")
+
+ media?.track(event = MediaSeekStartEvent(), player = MediaPlayerEntity(currentTime = 1.0))
+ media?.track(event = MediaSeekStartEvent(), player = MediaPlayerEntity(currentTime = 2.0))
+ media?.track(event = MediaSeekEndEvent(), player = MediaPlayerEntity(currentTime = 2.0))
+ media?.track(event = MediaSeekStartEvent(), player = MediaPlayerEntity(currentTime = 3.0))
+
+ Thread.sleep(100)
+
+ assertEquals(3, trackedEvents.size)
+ assertEquals(2, trackedEvents.filter { it.schema == eventSchema("seek_start") }.size)
+ }
+
+ @Test
+ fun doesntTrackEventsExcludedFromCaptureEvents() {
+ val configuration = MediaTrackingConfiguration(
+ id = "media1",
+ captureEvents = listOf(MediaPlayEvent::class)
+ )
+ val media = tracker?.media?.startMediaTracking(configuration = configuration)
+
+ media?.track(MediaPlayEvent())
+ media?.track(MediaPauseEvent())
+
+ Thread.sleep(100)
+
+ assertEquals(1, trackedEvents.size)
+ assertEquals(eventSchema("play"), firstEvent.schema)
+ }
+
+ @Test
+ fun addsEntitiesFromConfigToEvents() {
+ val configuration = MediaTrackingConfiguration(
+ id = "media1",
+ entities = listOf(
+ SelfDescribingJson("iglu:com.acme/track_type/jsonschema/1-0-0", mapOf("type" to "video"))
+ )
+ )
+ val media = tracker?.media?.startMediaTracking(configuration = configuration)
+
+ media?.track(MediaPlayEvent())
+
+ Thread.sleep(100)
+
+ assertEquals(1, trackedEvents.size)
+ assertNotNull(
+ firstEvent.entities.find {
+ it.map["schema"] == "iglu:com.acme/track_type/jsonschema/1-0-0"
+ }
+ )
+ assertNotNull(firstPlayer)
+ }
+
+ @Test
+ fun addsEntitiesTrackedWithEvent() {
+ val media = tracker?.media?.startMediaTracking(id = "media1")
+
+ media?.track(
+ event = MediaPlayEvent()
+ .entities(
+ listOf(
+ SelfDescribingJson("iglu:com.acme/track_type/jsonschema/1-0-0", mapOf("type" to "video"))
+ )
+ )
+ )
+
+ Thread.sleep(100)
+
+ assertEquals(1, trackedEvents.size)
+ assertNotNull(
+ firstEvent.entities.find {
+ it.map["schema"] == "iglu:com.acme/track_type/jsonschema/1-0-0"
+ }
+ )
+ assertNotNull(firstPlayer)
+ }
+
+ @Test
+ fun trackingPlaybackRateChangeEventUpdatesThePlaybackRate() {
+ val media = tracker?.media?.startMediaTracking(
+ id = "media1",
+ player = MediaPlayerEntity(playbackRate = 0.5)
+ )
+
+ media?.track(MediaPlaybackRateChangeEvent(newRate = 1.5))
+ media?.track(MediaPauseEvent())
+
+ Thread.sleep(100)
+
+ assertEquals(2, trackedEvents.size)
+ val rateEvent = trackedEvents.find { it.schema == eventSchema("playback_rate_change") }
+ assertEquals(0.5, rateEvent?.payload?.get("previousRate"))
+ assertEquals(1.5, rateEvent?.payload?.get("newRate"))
+ assertEquals(1.5, firstPlayer?.get("playbackRate"))
+ assertEquals(1.5, secondPlayer?.get("playbackRate"))
+ }
+
+ @Test
+ fun trackingVolumeChangeEventUpdatesTheVolume() {
+ val media = tracker?.media?.startMediaTracking(
+ id = "media1",
+ player = MediaPlayerEntity(volume = 80)
+ )
+
+ media?.track(MediaVolumeChangeEvent(newVolume = 90))
+ media?.track(MediaPauseEvent())
+
+ Thread.sleep(100)
+
+ assertEquals(2, trackedEvents.size)
+ val volumeEvent = trackedEvents.find { it.schema == eventSchema("volume_change") }
+ assertEquals(80, volumeEvent?.payload?.get("previousVolume"))
+ assertEquals(90, volumeEvent?.payload?.get("newVolume"))
+ assertEquals(90, firstPlayer?.get("volume"))
+ assertEquals(90, secondPlayer?.get("volume"))
+ }
+
+ @Test
+ fun trackingFullscreenChangeEventUpdatesFullscreenInPlayer() {
+ val media = tracker?.media?.startMediaTracking(
+ id = "media1",
+ player = MediaPlayerEntity(fullscreen = false)
+ )
+
+ media?.track(MediaFullscreenChangeEvent(fullscreen = true))
+
+ Thread.sleep(100)
+
+ assertEquals(true, firstEvent.payload.get("fullscreen"))
+ assertEquals(true, firstPlayer?.get("fullscreen"))
+ }
+
+ @Test
+ fun trackingPictureInPictureChangeEventUpdatesPictureInPictureInPlayer() {
+ val media = tracker?.media?.startMediaTracking(
+ id = "media1",
+ player = MediaPlayerEntity(pictureInPicture = false)
+ )
+
+ media?.track(MediaPictureInPictureChangeEvent(pictureInPicture = true))
+
+ Thread.sleep(100)
+
+ assertEquals(true, firstEvent.payload.get("pictureInPicture"))
+ assertEquals(true, firstPlayer?.get("pictureInPicture"))
+ }
+
+ @Test
+ fun trackingAdFirstQuartileSetsPercentProgress() {
+ val media = tracker?.media?.startMediaTracking(id = "media1")
+
+ media?.track(MediaAdFirstQuartileEvent())
+
+ Thread.sleep(100)
+
+ assertEquals(25, firstEvent.payload.get("percentProgress"))
+ }
+
+ @Test
+ fun trackingAdMidpointSetsPercentProgress() {
+ val media = tracker?.media?.startMediaTracking(id = "media1")
+
+ media?.track(MediaAdMidpointEvent())
+
+ Thread.sleep(100)
+
+ assertEquals(50, firstEvent.payload.get("percentProgress"))
+ }
+
+ @Test
+ fun trackingAdThirdQuartileSetsPercentProgress() {
+ val media = tracker?.media?.startMediaTracking(id = "media1")
+
+ media?.track(MediaAdThirdQuartileEvent())
+
+ Thread.sleep(100)
+
+ assertEquals(75, firstEvent.payload.get("percentProgress"))
+ }
+
+ @Test
+ fun addsPercentProgressPropertyToAdEvents() {
+ val media = tracker?.media?.startMediaTracking(id = "media1")
+
+ media?.track(MediaAdClickEvent(percentProgress = 15))
+ media?.track(MediaAdSkipEvent(percentProgress = 30))
+ media?.track(MediaAdResumeEvent(percentProgress = 40))
+ media?.track(MediaAdPauseEvent(percentProgress = 50))
+
+ Thread.sleep(100)
+
+ val adClickEvent = trackedEvents.find { it.schema == eventSchema("ad_click") }
+ assertEquals(15, adClickEvent?.payload?.get("percentProgress"))
+ val adSkipEvent = trackedEvents.find { it.schema == eventSchema("ad_skip") }
+ assertEquals(30, adSkipEvent?.payload?.get("percentProgress"))
+ val adResumeEvent = trackedEvents.find { it.schema == eventSchema("ad_resume") }
+ assertEquals(40, adResumeEvent?.payload?.get("percentProgress"))
+ val adPauseEvent = trackedEvents.find { it.schema == eventSchema("ad_pause") }
+ assertEquals(50, adPauseEvent?.payload?.get("percentProgress"))
+ }
+
+ @Test
+ fun setsQualityPropertiesInQualityChangeEvent() {
+ val media = tracker?.media?.startMediaTracking(
+ id = "media1",
+ player = MediaPlayerEntity(quality = "720p")
+ )
+
+ media?.track(MediaQualityChangeEvent(
+ newQuality = "1080p",
+ bitrate = 3333,
+ framesPerSecond = 60
+ ))
+
+ Thread.sleep(100)
+
+ assertEquals("720p", firstEvent.payload.get("previousQuality"))
+ assertEquals("1080p", firstEvent.payload.get("newQuality"))
+ assertEquals(3333, firstEvent.payload.get("bitrate"))
+ assertEquals(60, firstEvent.payload.get("framesPerSecond"))
+ }
+
+ @Test
+ fun setsPropertiesOfErrorEvent() {
+ val media = tracker?.media?.startMediaTracking(id = "media1")
+
+ media?.track(MediaErrorEvent(
+ errorCode = "501",
+ errorName = "forbidden",
+ errorDescription = "Failed to load media"
+ ))
+
+ Thread.sleep(100)
+
+ assertEquals("501", firstEvent.payload.get("errorCode"))
+ assertEquals("forbidden", firstEvent.payload.get("errorName"))
+ assertEquals("Failed to load media", firstEvent.payload.get("errorDescription"))
+ }
+
+ @Test
+ fun tracksCustomEvent() {
+ val media = tracker?.media?.startMediaTracking(
+ id = "media1",
+ player = MediaPlayerEntity(label = "Video")
+ )
+
+ media?.track(SelfDescribing(
+ "iglu:com.acme/video_played/jsonschema/1-0-0",
+ mapOf("url" to "https://www.youtube.com/watch?v=12345")
+ ))
+
+ Thread.sleep(100)
+
+ assertEquals("iglu:com.acme/video_played/jsonschema/1-0-0", firstEvent.schema)
+ assertEquals("Video", firstPlayer?.get("label"))
+ }
+
+ // --- SESSION
+
+ @Test
+ fun addsSessionContextEntityWithinGivenID() {
+ val media = tracker?.media?.startMediaTracking(id = "media1")
+
+ media?.track(MediaPlayEvent())
+
+ Thread.sleep(100)
+
+ assertEquals("media1", firstSession?.get("mediaSessionId"))
+ }
+
+ @Test
+ fun calculatesSessionStats() {
+ val timeTraveler = TimeTraveler()
+ val session = MediaSessionTracking(
+ id = "media1",
+ dateGenerator = { timeTraveler.generateDate() },
+ )
+ val media = MediaTrackingImpl(
+ id = "media1",
+ tracker = tracker!!,
+ player = MediaPlayerEntity(duration = 10.0),
+ session = session,
+ )
+
+ media.track(MediaPlayEvent())
+ timeTraveler.travelBy(10.toDuration(DurationUnit.SECONDS))
+ media.update(player = MediaPlayerEntity(currentTime = 10.0))
+ media.track(MediaEndEvent())
+
+ Thread.sleep(100)
+
+ val endEvent = trackedEvents.find { it.schema == eventSchema("end") }
+ val lastSession = endEvent?.entities?.find { it.map.get("schema") == sessionSchema }?.map?.get("data") as? Map<*, *>
+ assertNotNull(lastSession)
+ assertEquals(10.0, lastSession?.get("timePlayed"))
+ assertEquals(11.0, lastSession?.get("contentWatched"))
+ }
+
+ // --- PING EVENTS
+
+ @Test
+ fun startsSendingPingEventsAfterSessionStarts() {
+ val timer = createTimer()
+ val pingInterval = MediaPingInterval(
+ pingInterval = 10,
+ createTimer = { timer },
+ )
+ MediaTrackingImpl(
+ id = "media1",
+ tracker = tracker!!,
+ pingInterval = pingInterval,
+ )
+
+ timer.fire()
+
+ Thread.sleep(100)
+
+ assertEquals(1, trackedEvents.size)
+ assertEquals(10000L, timer.delay)
+ assertEquals(10000L, timer.period)
+ assertEquals(eventSchema("ping"), firstEvent.schema)
+ }
+
+ @Test
+ fun shouldSendPingEventsRegardlessOfOtherEvents() {
+ val timer = createTimer()
+ val pingInterval = MediaPingInterval(
+ createTimer = { timer },
+ )
+ val media = MediaTrackingImpl(
+ id = "media1",
+ tracker = tracker!!,
+ pingInterval = pingInterval,
+ )
+
+ media.track(MediaPlayEvent())
+ timer.fire()
+ media.track(MediaPauseEvent())
+ timer.fire()
+
+ Thread.sleep(100)
+
+ assertEquals(4, trackedEvents.size)
+ }
+
+ @Test
+ fun shouldStopSendingPingEventsWhenPaused() {
+ val timer = createTimer()
+ val pingInterval = MediaPingInterval(
+ createTimer = { timer },
+ maxPausedPings = 2
+ )
+ val media = MediaTrackingImpl(
+ id = "media1",
+ tracker = tracker!!,
+ pingInterval = pingInterval,
+ )
+
+ media.update(player = MediaPlayerEntity(paused = true))
+ for (i in 0 until 5) {
+ timer.fire()
+ }
+
+ Thread.sleep(100)
+
+ assertEquals(2, trackedEvents.size)
+ }
+
+ @Test
+ fun shouldNotStopSendingPingEventsWhenPlaying() {
+ val timer = createTimer()
+ val pingInterval = MediaPingInterval(
+ createTimer = { timer },
+ maxPausedPings = 2
+ )
+ val media = MediaTrackingImpl(
+ id = "media1",
+ tracker = tracker!!,
+ pingInterval = pingInterval,
+ )
+
+ media.update(player = MediaPlayerEntity(paused = false))
+ for (i in 0 until 5) {
+ timer.fire()
+ }
+
+ Thread.sleep(100)
+
+ assertEquals(5, trackedEvents.size)
+ }
+
+ // --- PERCENT PROGRESS
+
+ @Test
+ fun shouldSendProgressEventsWhenBoundariesReached() {
+ val configuration = MediaTrackingConfiguration(
+ id = "media1",
+ player = MediaPlayerEntity(duration = 100.0),
+ boundaries = listOf(10, 50, 90),
+ )
+ val media = tracker?.media?.startMediaTracking(configuration = configuration)
+
+ media?.track(MediaPlayEvent())
+ for (i in 1 until 100) {
+ media?.update(player = MediaPlayerEntity(currentTime = i.toDouble()))
+ }
+
+ Thread.sleep(100)
+
+ assertEquals(4, trackedEvents.size)
+ assertEquals(3, trackedEvents.filter { it.schema == eventSchema("percent_progress") }.size)
+ }
+
+ @Test
+ fun doesntSendProgressEventsIfPaused() {
+ val configuration = MediaTrackingConfiguration(
+ id = "media1",
+ player = MediaPlayerEntity(duration = 100.0),
+ boundaries = listOf(10, 50, 90),
+ )
+ val media = tracker?.media?.startMediaTracking(configuration = configuration)
+
+ media?.track(MediaPauseEvent())
+ for (i in 1 until 100) {
+ media?.update(player = MediaPlayerEntity(currentTime = i.toDouble()))
+ }
+
+ Thread.sleep(100)
+
+ assertEquals(1, trackedEvents.size)
+ }
+
+ @Test
+ fun doesntSendProgressEventMultipleTimes() {
+ val configuration = MediaTrackingConfiguration(
+ id = "media1",
+ player = MediaPlayerEntity(duration = 100.0),
+ boundaries = listOf(10, 50, 90),
+ )
+ val media = tracker?.media?.startMediaTracking(configuration = configuration)
+
+ media?.track(MediaPlayEvent())
+ for (i in 1 until 100) {
+ media?.update(player = MediaPlayerEntity(currentTime = i.toDouble()))
+ }
+
+ media?.track(MediaSeekStartEvent(), player = MediaPlayerEntity(currentTime = 0.0))
+ for (i in 1 until 100) {
+ media?.update(player = MediaPlayerEntity(currentTime = i.toDouble()))
+ }
+
+ Thread.sleep(100)
+
+ assertEquals(5, trackedEvents.size)
+ assertEquals(3, trackedEvents.filter { it.schema == eventSchema("percent_progress") }.size)
+ }
+
+ // --- PRIVATE
+ private val context: Context
+ get() = InstrumentationRegistry.getInstrumentation().targetContext
+
+ private fun createTracker(): TrackerController {
+ val namespace = "ns" + Math.random().toString()
+ val networkConfig = NetworkConfiguration(MockNetworkConnection(HttpMethod.POST, 200))
+ val trackerConfig = TrackerConfiguration("appId")
+ .installAutotracking(false)
+ .lifecycleAutotracking(false)
+
+ val plugin = PluginConfiguration("plugin")
+ plugin.afterTrack {
+ if (namespace == this.tracker?.namespace) {
+ trackedEvents.add(it)
+ }
+ }
+
+ return createTracker(
+ context,
+ namespace = namespace,
+ network = networkConfig,
+ trackerConfig,
+ plugin
+ )
+ }
+
+ private fun createTimer(): TestTimerInterface {
+ return object : TestTimerInterface {
+ private var task: TimerTask? = null
+ override var delay: Long? = null
+ override var period: Long? = null
+
+ override fun schedule(task: TimerTask, delay: Long, period: Long) {
+ this.task = task
+ this.delay = delay
+ this.period = period
+ }
+
+ override fun cancel() {
+ throw NotImplementedError()
+ }
+
+ override fun fire() {
+ task?.run()
+ }
+ }
+ }
+}
+
+interface TestTimerInterface : TimerInterface {
+ var delay: Long?
+ var period: Long?
+ fun fire()
+}
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/NetworkConnectionTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/NetworkConnectionTest.kt
index b7779f1b2..94ca1ec7a 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/NetworkConnectionTest.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/NetworkConnectionTest.kt
@@ -264,6 +264,38 @@ class NetworkConnectionTest {
mockServer.shutdown()
}
+ @Test
+ @Throws(IOException::class, InterruptedException::class)
+ fun testAddsCustomRequestHeadersForPostRequest() {
+ val mockServer = getMockServer(200)
+ val connection = OkHttpNetworkConnectionBuilder(getMockServerURI(mockServer)!!, context)
+ .method(HttpMethod.POST)
+ .requestHeaders(mapOf("foo" to "bar"))
+ .build()
+ val payload: Payload = TrackerPayload()
+ payload.add("key", "value")
+ connection.sendRequests(listOf(Request(payload, 2)))
+ val req = mockServer.takeRequest(60, TimeUnit.SECONDS)
+ Assert.assertEquals("bar", req!!.getHeader("foo"))
+ mockServer.shutdown()
+ }
+
+ @Test
+ @Throws(IOException::class, InterruptedException::class)
+ fun testAddsCustomRequestHeadersForGetRequest() {
+ val mockServer = getMockServer(200)
+ val connection = OkHttpNetworkConnectionBuilder(getMockServerURI(mockServer)!!, context)
+ .method(HttpMethod.GET)
+ .requestHeaders(mapOf("foo" to "bar"))
+ .build()
+ val payload: Payload = TrackerPayload()
+ payload.add("key", "value")
+ connection.sendRequests(listOf(Request(payload, 2)))
+ val req = mockServer.takeRequest(60, TimeUnit.SECONDS)
+ Assert.assertEquals("bar", req!!.getHeader("foo"))
+ mockServer.shutdown()
+ }
+
// Service methods
private fun assertGETRequest(req: RecordedRequest?) {
Assert.assertNotNull(req)
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/ServiceProviderTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/ServiceProviderTest.kt
index 3ae51cbea..6383fb945 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/ServiceProviderTest.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/ServiceProviderTest.kt
@@ -15,9 +15,9 @@ package com.snowplowanalytics.snowplow.tracker
import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
-import com.snowplowanalytics.core.emitter.EmitterConfigurationUpdate
import com.snowplowanalytics.core.tracker.ServiceProvider
import com.snowplowanalytics.snowplow.configuration.Configuration
+import com.snowplowanalytics.snowplow.configuration.EmitterConfiguration
import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration
import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration
import com.snowplowanalytics.snowplow.controller.TrackerController
@@ -51,7 +51,7 @@ class ServiceProviderTest {
// refresh configuration
val configurationUpdates: MutableList = ArrayList()
- configurationUpdates.add(EmitterConfigurationUpdate())
+ configurationUpdates.add(EmitterConfiguration())
provider.reset(configurationUpdates)
// track event and check that emitter is paused
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/integration/EventSendingTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/integration/EventSendingTest.kt
index 2808d7875..cfe366d6f 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/integration/EventSendingTest.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/integration/EventSendingTest.kt
@@ -20,7 +20,7 @@ import com.snowplowanalytics.core.emitter.Emitter
import com.snowplowanalytics.core.emitter.storage.SQLiteEventStore
import com.snowplowanalytics.core.tracker.Subject
import com.snowplowanalytics.core.tracker.Tracker
-import com.snowplowanalytics.snowplow.TestUtils.createSessionSharedPreferences
+import com.snowplowanalytics.snowplow.util.TestUtils.createSessionSharedPreferences
import com.snowplowanalytics.snowplow.emitter.BufferOption
import com.snowplowanalytics.snowplow.emitter.EventStore
import com.snowplowanalytics.snowplow.event.*
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/TestUtils.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/util/TestUtils.kt
similarity index 97%
rename from snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/TestUtils.kt
rename to snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/util/TestUtils.kt
index 09541ed71..99c6163a4 100644
--- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/TestUtils.kt
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/util/TestUtils.kt
@@ -10,7 +10,7 @@
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
-package com.snowplowanalytics.snowplow
+package com.snowplowanalytics.snowplow.util
import android.content.Context
import com.snowplowanalytics.core.constants.Parameters
diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/util/TimeTraveler.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/util/TimeTraveler.kt
new file mode 100644
index 000000000..d774fa789
--- /dev/null
+++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/util/TimeTraveler.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.util
+
+import java.util.*
+import kotlin.time.Duration
+
+class TimeTraveler {
+ private var date: Date = Date()
+
+ fun travelBy(duration: Duration) {
+ date = Date(date.time + duration.inWholeMilliseconds)
+ }
+
+ fun generateDate(): Date {
+ return date
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/Emitter.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/Emitter.kt
index 34b2e2d59..0214e48a7 100755
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/Emitter.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/Emitter.kt
@@ -158,6 +158,7 @@ class Emitter(context: Context, collectorUri: String, builder: ((Emitter) -> Uni
.client(client)
.cookieJar(cookieJar)
.serverAnonymisation(serverAnonymisation)
+ .requestHeaders(requestHeaders)
.build()
}
}
@@ -183,6 +184,7 @@ class Emitter(context: Context, collectorUri: String, builder: ((Emitter) -> Uni
.client(client)
.cookieJar(cookieJar)
.serverAnonymisation(serverAnonymisation)
+ .requestHeaders(requestHeaders)
.build()
}
@@ -208,7 +210,7 @@ class Emitter(context: Context, collectorUri: String, builder: ((Emitter) -> Uni
/**
* The request security selected for the emitter
*/
- var requestSecurity: Protocol = EmitterDefaults.requestSecurity
+ var requestSecurity: Protocol = EmitterDefaults.httpProtocol
/**
* Sets the Protocol for the Emitter
* @param security the Protocol
@@ -225,6 +227,7 @@ class Emitter(context: Context, collectorUri: String, builder: ((Emitter) -> Uni
.client(client)
.cookieJar(cookieJar)
.serverAnonymisation(serverAnonymisation)
+ .requestHeaders(requestHeaders)
.build()
}
@@ -263,6 +266,7 @@ class Emitter(context: Context, collectorUri: String, builder: ((Emitter) -> Uni
.client(client)
.cookieJar(cookieJar)
.serverAnonymisation(serverAnonymisation)
+ .requestHeaders(requestHeaders)
.build()
}
}
@@ -284,6 +288,7 @@ class Emitter(context: Context, collectorUri: String, builder: ((Emitter) -> Uni
.client(client)
.cookieJar(cookieJar)
.serverAnonymisation(serverAnonymisation)
+ .requestHeaders(requestHeaders)
.build()
}
@@ -319,6 +324,7 @@ class Emitter(context: Context, collectorUri: String, builder: ((Emitter) -> Uni
.client(client)
.cookieJar(cookieJar)
.serverAnonymisation(serverAnonymisation)
+ .requestHeaders(requestHeaders)
.build()
}
}
@@ -331,6 +337,32 @@ class Emitter(context: Context, collectorUri: String, builder: ((Emitter) -> Uni
_customRetryForStatusCodes.set(value ?: HashMap())
}
+ /**
+ * The request headers for the emitter
+ */
+ var requestHeaders: Map? = null
+ /**
+ * Updates the request headers for the emitter.
+ * Ignored if using a custom network connection.
+ */
+ set(requestHeaders) {
+ field = requestHeaders
+ if (!isCustomNetworkConnection && builderFinished) {
+ networkConnection = emitTimeout?.let {
+ OkHttpNetworkConnectionBuilder(uri, context)
+ .method(httpMethod)
+ .tls(tlsVersions)
+ .emitTimeout(it)
+ .customPostPath(customPostPath)
+ .client(client)
+ .cookieJar(cookieJar)
+ .serverAnonymisation(serverAnonymisation)
+ .requestHeaders(requestHeaders)
+ .build()
+ }
+ }
+ }
+
/**
* Creates an emitter object
*/
@@ -356,6 +388,7 @@ class Emitter(context: Context, collectorUri: String, builder: ((Emitter) -> Uni
.client(client)
.cookieJar(cookieJar)
.serverAnonymisation(serverAnonymisation)
+ .requestHeaders(requestHeaders)
.build()
}
} else {
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterConfigurationUpdate.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterConfigurationUpdate.kt
deleted file mode 100644
index 11a68b63a..000000000
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterConfigurationUpdate.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
- *
- * This program is licensed to you under the Apache License Version 2.0,
- * and you may not use this file except in compliance with the Apache License Version 2.0.
- * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the Apache License Version 2.0 is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
- */
-package com.snowplowanalytics.core.emitter
-
-import com.snowplowanalytics.snowplow.configuration.EmitterConfiguration
-import com.snowplowanalytics.snowplow.emitter.BufferOption
-import com.snowplowanalytics.snowplow.emitter.EventStore
-import com.snowplowanalytics.snowplow.network.RequestCallback
-
-class EmitterConfigurationUpdate : EmitterConfiguration() {
- var sourceConfig: EmitterConfiguration? = null
- var isPaused = false
- private var bufferOptionUpdated = false
- private var emitRangeUpdated = false
- private var threadPoolSizeUpdated = false
- private var byteLimitGetUpdated = false
- private var byteLimitPostUpdated = false
- private var customRetryForStatusCodesUpdated = false
- private var serverAnonymisationUpdated = false
-
- override var eventStore: EventStore?
- get() = if (sourceConfig == null) null else sourceConfig!!.eventStore
- set(value) {
- // can't set a new eventStore
- }
-
- override var requestCallback: RequestCallback?
- get() = if (sourceConfig == null) null else sourceConfig!!.requestCallback
- set(value) {
- // can't set a new requestCallback
- }
-
- override var bufferOption: BufferOption
- get() = if (sourceConfig == null || bufferOptionUpdated) super.bufferOption else sourceConfig!!.bufferOption
- set(value) {
- super.bufferOption = value
- bufferOptionUpdated = true
- }
-
- override var threadPoolSize: Int
- get() = if (sourceConfig == null || threadPoolSizeUpdated) super.threadPoolSize else sourceConfig!!.threadPoolSize
- set(value) {
- super.threadPoolSize = value
- threadPoolSizeUpdated = true
- }
-
- override var byteLimitGet: Long
- get() = if (sourceConfig == null || byteLimitGetUpdated) super.byteLimitGet else sourceConfig!!.byteLimitGet
- set(value) {
- super.byteLimitGet = value
- byteLimitGetUpdated = true
- }
-
- override var byteLimitPost: Long
- get() = if (sourceConfig == null || byteLimitPostUpdated) super.byteLimitPost else sourceConfig!!.byteLimitPost
- set(value) {
- super.byteLimitPost = value
- byteLimitPostUpdated = true
- }
-
- override var customRetryForStatusCodes: Map?
- get() = if (sourceConfig == null || customRetryForStatusCodesUpdated) super.customRetryForStatusCodes else sourceConfig!!.customRetryForStatusCodes
- set(value) {
- super.customRetryForStatusCodes = value
- customRetryForStatusCodesUpdated = true
- }
-
- override var serverAnonymisation: Boolean
- get() = if (sourceConfig == null || serverAnonymisationUpdated) super.serverAnonymisation else sourceConfig!!.serverAnonymisation
- set(value) {
- super.serverAnonymisation = value
- serverAnonymisationUpdated = true
- }
-
- override var emitRange: Int
- get() = if (sourceConfig == null || emitRangeUpdated) super.emitRange else sourceConfig!!.emitRange
- set(value) {
- super.emitRange = value
- emitRangeUpdated = true
- }
-}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterControllerImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterControllerImpl.kt
index d9ec47e64..792632c5b 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterControllerImpl.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterControllerImpl.kt
@@ -16,6 +16,7 @@ import androidx.annotation.RestrictTo
import com.snowplowanalytics.core.Controller
import com.snowplowanalytics.core.tracker.Logger
import com.snowplowanalytics.core.tracker.ServiceProviderInterface
+import com.snowplowanalytics.snowplow.configuration.EmitterConfiguration
import com.snowplowanalytics.snowplow.controller.EmitterController
import com.snowplowanalytics.snowplow.emitter.BufferOption
import com.snowplowanalytics.snowplow.emitter.EventStore
@@ -64,11 +65,17 @@ class EmitterControllerImpl(serviceProvider: ServiceProviderInterface) :
override var requestCallback: RequestCallback?
get() = emitter.requestCallback
- set(requestCallback) { emitter.requestCallback = requestCallback }
+ set(requestCallback) {
+ dirtyConfig.requestCallback = requestCallback
+ emitter.requestCallback = requestCallback
+ }
override var customRetryForStatusCodes: Map?
get() = emitter.customRetryForStatusCodes
- set(customRetryForStatusCodes) { emitter.customRetryForStatusCodes = customRetryForStatusCodes }
+ set(customRetryForStatusCodes) {
+ dirtyConfig.customRetryForStatusCodes = customRetryForStatusCodes
+ emitter.customRetryForStatusCodes = customRetryForStatusCodes
+ }
override var serverAnonymisation: Boolean
get() = emitter.serverAnonymisation
@@ -101,8 +108,8 @@ class EmitterControllerImpl(serviceProvider: ServiceProviderInterface) :
}
// Private methods
- private val dirtyConfig: EmitterConfigurationUpdate
- get() = serviceProvider.emitterConfigurationUpdate
+ private val dirtyConfig: EmitterConfiguration
+ get() = serviceProvider.emitterConfiguration
companion object {
private val TAG = EmitterControllerImpl::class.java.simpleName
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterDefaults.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterDefaults.kt
index fcf4176ad..dd2d8e40a 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterDefaults.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/EmitterDefaults.kt
@@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit
object EmitterDefaults {
var httpMethod = HttpMethod.POST
var bufferOption = BufferOption.DefaultGroup
- var requestSecurity = Protocol.HTTPS
+ var httpProtocol = Protocol.HTTPS
var tlsVersions: EnumSet = EnumSet.of(TLSVersion.TLSv1_2)
var emitRange: Int = 150
var emitterTick = 5
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkConfigurationInterface.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkConfigurationInterface.kt
index 2be442bd7..a611a503b 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkConfigurationInterface.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkConfigurationInterface.kt
@@ -20,12 +20,35 @@ import okhttp3.CookieJar
import okhttp3.OkHttpClient
interface NetworkConfigurationInterface {
+ /** URL (without schema/protocol) used to send events to the collector. */
val endpoint: String?
+ /** Method used to send events to the collector. */
val method: HttpMethod?
+ /** Protocol used to send events to the collector. */
val protocol: Protocol?
+ /** Custom `NetworkConnection` instance to use for sending events. */
val networkConnection: NetworkConnection?
+ /** A custom path which will be added to the endpoint URL to specify the complete URL of the collector when paired with the POST method. */
val customPostPath: String?
+ /**
+ * The timeout set for the requests to the collector.
+ * The maximum timeout for emitting events. If emit time exceeds this value
+ * TimeOutException will be thrown.
+ */
val timeout: Int?
+ /**
+ * An OkHttp client that will be used in the emitter. You can provide your
+ * own if you want to share your Singleton client's interceptors, connection pool etc.
+ * By default a new [OkHttpClient] is created when the tracker is instantiated.
+ */
val okHttpClient: OkHttpClient?
+ /**
+ * An OkHttp cookie jar to override the default
+ * [CollectorCookieJar](com.snowplowanalytics.snowplow.network.CollectorCookieJar)
+ * that stores cookies in SharedPreferences.
+ * A cookie jar provided here will be ignored if a custom `okHttpClient` is configured.
+ */
val okHttpCookieJar: CookieJar?
+ /** Custom headers to add to HTTP requests to the collector. */
+ val requestHeaders: Map?
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkConfigurationUpdate.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkConfigurationUpdate.kt
deleted file mode 100644
index 07849e306..000000000
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkConfigurationUpdate.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
- *
- * This program is licensed to you under the Apache License Version 2.0,
- * and you may not use this file except in compliance with the Apache License Version 2.0.
- * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the Apache License Version 2.0 is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
- */
-package com.snowplowanalytics.core.emitter
-
-import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration
-import com.snowplowanalytics.snowplow.network.HttpMethod
-import com.snowplowanalytics.snowplow.network.NetworkConnection
-import com.snowplowanalytics.snowplow.network.Protocol
-import okhttp3.CookieJar
-import okhttp3.OkHttpClient
-
-class NetworkConfigurationUpdate : NetworkConfigurationInterface {
- var sourceConfig: NetworkConfiguration? = null
- var customPostPathUpdated = false
-
- override var customPostPath: String? = null
- get() = if (sourceConfig == null || customPostPathUpdated) field else sourceConfig!!.customPostPath
-
- override val endpoint: String?
- get() = if (sourceConfig == null) null else sourceConfig!!.endpoint
-
- override val method: HttpMethod?
- get() = if (sourceConfig == null) null else sourceConfig!!.method
-
- override val protocol: Protocol?
- get() = if (sourceConfig == null) null else sourceConfig!!.protocol
-
- override val networkConnection: NetworkConnection?
- get() = if (sourceConfig == null) null else sourceConfig!!.networkConnection
-
- override val timeout: Int?
- get() = if (sourceConfig == null) null else sourceConfig!!.timeout
-
- override val okHttpClient: OkHttpClient?
- get() = if (sourceConfig == null) null else sourceConfig!!.okHttpClient
-
- override val okHttpCookieJar: CookieJar?
- get() = if (sourceConfig == null) null else sourceConfig!!.okHttpCookieJar
-}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkControllerImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkControllerImpl.kt
index c3714a2f0..7ba78052d 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkControllerImpl.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/emitter/NetworkControllerImpl.kt
@@ -15,6 +15,7 @@ package com.snowplowanalytics.core.emitter
import androidx.annotation.RestrictTo
import com.snowplowanalytics.core.Controller
import com.snowplowanalytics.core.tracker.ServiceProviderInterface
+import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration
import com.snowplowanalytics.snowplow.controller.NetworkController
import com.snowplowanalytics.snowplow.network.HttpMethod
import com.snowplowanalytics.snowplow.network.OkHttpNetworkConnection
@@ -46,7 +47,6 @@ class NetworkControllerImpl(serviceProvider: ServiceProviderInterface) :
get() = emitter.customPostPath
set(customPostPath) {
dirtyConfig.customPostPath = customPostPath
- dirtyConfig.customPostPathUpdated = true
emitter.customPostPath = customPostPath
}
@@ -60,6 +60,6 @@ class NetworkControllerImpl(serviceProvider: ServiceProviderInterface) :
private val emitter: Emitter
get() = serviceProvider.getOrMakeEmitter()
- private val dirtyConfig: NetworkConfigurationUpdate
- get() = serviceProvider.networkConfigurationUpdate
+ private val dirtyConfig: NetworkConfiguration
+ get() = serviceProvider.networkConfiguration
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/gdpr/GdprConfigurationUpdate.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/gdpr/GdprConfigurationUpdate.kt
deleted file mode 100644
index 3fb78a61e..000000000
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/gdpr/GdprConfigurationUpdate.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
- *
- * This program is licensed to you under the Apache License Version 2.0,
- * and you may not use this file except in compliance with the Apache License Version 2.0.
- * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the Apache License Version 2.0 is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
- */
-package com.snowplowanalytics.core.gdpr
-
-import com.snowplowanalytics.snowplow.configuration.GdprConfiguration
-import com.snowplowanalytics.snowplow.util.Basis
-
-class GdprConfigurationUpdate : GdprConfiguration(
- Basis.CONTRACT,
- null,
- null,
- null) {
-
- var sourceConfig: GdprConfiguration? = null
- var isEnabled = false
- private var gdprUpdated = false
-
- var gdpr: Gdpr? = null
- set(value) {
- field = value
- gdprUpdated = true
- }
-
- override val basisForProcessing: Basis
- get() = if (sourceConfig == null || gdprUpdated) super.basisForProcessing else sourceConfig!!.basisForProcessing
-
- override val documentId: String?
- get() = if (sourceConfig == null || gdprUpdated) super.documentId else sourceConfig!!.documentId
-
- override val documentVersion: String?
- get() = if (sourceConfig == null || gdprUpdated) super.documentVersion else sourceConfig!!.documentVersion
-
- override val documentDescription: String?
- get() = if (sourceConfig == null || gdprUpdated) super.documentDescription else sourceConfig!!.documentDescription
-}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/gdpr/GdprControllerImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/gdpr/GdprControllerImpl.kt
index 10aebc057..397804d81 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/gdpr/GdprControllerImpl.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/gdpr/GdprControllerImpl.kt
@@ -16,6 +16,7 @@ import androidx.annotation.RestrictTo
import com.snowplowanalytics.core.Controller
import com.snowplowanalytics.core.tracker.ServiceProviderInterface
import com.snowplowanalytics.core.tracker.Tracker
+import com.snowplowanalytics.snowplow.configuration.GdprConfiguration
import com.snowplowanalytics.snowplow.controller.GdprController
import com.snowplowanalytics.snowplow.util.Basis
@@ -76,6 +77,6 @@ class GdprControllerImpl(serviceProvider: ServiceProviderInterface) : Controller
// Private methods
private val tracker: Tracker
get() = serviceProvider.getOrMakeTracker()
- private val dirtyConfig: GdprConfigurationUpdate
- get() = serviceProvider.gdprConfigurationUpdate
+ private val dirtyConfig: GdprConfiguration
+ get() = serviceProvider.gdprConfiguration
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/MediaSchemata.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/MediaSchemata.kt
new file mode 100644
index 000000000..102707499
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/MediaSchemata.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media
+
+object MediaSchemata {
+ private val schemaPrefix = "iglu:com.snowplowanalytics.snowplow.media/"
+ private val schemaSuffix = "/jsonschema/1-0-0"
+
+ val playerSchema = "${schemaPrefix}player$schemaSuffix"
+ val sessionSchema = "${schemaPrefix}session$schemaSuffix"
+ val adSchema = "${schemaPrefix}ad$schemaSuffix"
+ val adBreakSchema = "${schemaPrefix}ad_break$schemaSuffix"
+
+ fun eventSchema(eventName: String) = "${schemaPrefix}${eventName}_event$schemaSuffix"
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaAdTracking.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaAdTracking.kt
new file mode 100644
index 000000000..732b87a55
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaAdTracking.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.controller
+
+import com.snowplowanalytics.snowplow.event.Event
+import com.snowplowanalytics.snowplow.media.entity.MediaAdBreakEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaAdEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import com.snowplowanalytics.snowplow.media.event.*
+import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
+
+class MediaAdTracking {
+
+ var ad: MediaAdEntity? = null
+ var adBreak: MediaAdBreakEntity? = null
+ private var podPosition = 0
+
+ val entities: List
+ get() = listOfNotNull(ad?.entity, adBreak?.entity)
+
+ fun updateForThisEvent(event: Event?, player: MediaPlayerEntity, ad: MediaAdEntity? = null, adBreak: MediaAdBreakEntity? = null) {
+ when (event) {
+ is MediaAdStartEvent -> {
+ this.ad = null
+ podPosition += 1
+ }
+ is MediaAdBreakStartEvent -> {
+ this.adBreak = null
+ podPosition = 0
+ }
+ }
+
+ ad?.let {
+ this.ad?.update(it)
+ this.ad = this.ad ?: it
+ if (podPosition > 0) {
+ this.ad?.podPosition = podPosition
+ }
+ }
+
+ adBreak?.let {
+ this.adBreak?.update(it)
+ this.adBreak = this.adBreak ?: it
+ this.adBreak?.update(player)
+ }
+ }
+
+ fun updateForNextEvent(event: Event?) {
+ when (event) {
+ is MediaAdBreakEndEvent -> {
+ adBreak = null
+ podPosition = 0
+ }
+
+ is MediaAdCompleteEvent, is MediaAdSkipEvent -> {
+ ad = null
+ }
+ }
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaControllerImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaControllerImpl.kt
new file mode 100644
index 000000000..946c5be5e
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaControllerImpl.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.controller
+
+import com.snowplowanalytics.core.Controller
+import com.snowplowanalytics.core.tracker.ServiceProvider
+import com.snowplowanalytics.snowplow.media.configuration.MediaTrackingConfiguration
+import com.snowplowanalytics.snowplow.media.controller.MediaController
+import com.snowplowanalytics.snowplow.media.controller.MediaTracking
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+class MediaControllerImpl(
+ serviceProvider: ServiceProvider
+): Controller(serviceProvider), MediaController {
+ private var mediaTrackings = mutableMapOf()
+
+ override fun startMediaTracking(id: String, player: MediaPlayerEntity?): MediaTracking {
+ val configuration = MediaTrackingConfiguration(id = id, player = player)
+ return startMediaTracking(configuration = configuration)
+ }
+
+ override fun startMediaTracking(configuration: MediaTrackingConfiguration): MediaTracking {
+ val session = if (configuration.session) {
+ MediaSessionTracking(
+ id = configuration.id,
+ pingInterval = configuration.pingInterval,
+ )
+ } else {
+ null
+ }
+
+ val pingInterval = if (configuration.pings) {
+ MediaPingInterval(
+ pingInterval = configuration.pingInterval,
+ maxPausedPings = configuration.maxPausedPings,
+ )
+ } else {
+ null
+ }
+
+ val mediaTracking = MediaTrackingImpl(
+ id = configuration.id,
+ tracker = serviceProvider.getOrMakeTrackerController(),
+ player = configuration.player,
+ session = session,
+ pingInterval = pingInterval,
+ boundaries = configuration.boundaries,
+ captureEvents = configuration.captureEvents,
+ customEntities = configuration.entities,
+ )
+
+ mediaTrackings[configuration.id] = mediaTracking
+
+ return mediaTracking
+ }
+
+ override fun getMediaTracking(id: String): MediaTracking? {
+ return mediaTrackings[id]
+ }
+
+ override fun endMediaTracking(id: String) {
+ mediaTrackings[id]?.end()
+ mediaTrackings.remove(id)
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaPingInterval.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaPingInterval.kt
new file mode 100644
index 000000000..37a0bd331
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaPingInterval.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.controller
+
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import java.util.*
+
+interface TimerInterface {
+ fun schedule(task: TimerTask, delay: Long, period: Long)
+ fun cancel()
+}
+
+class MediaPingInterval(
+ pingInterval: Int? = null,
+ maxPausedPings: Int? = null,
+ private var createTimer: (() -> TimerInterface)? = null,
+) {
+ val pingInterval = pingInterval ?: 30
+
+ private var paused: Boolean? = null
+ private var numPausedPings: Int = 0
+ private val maxPausedPings = maxPausedPings ?: 1
+ private val isPaused: Boolean
+ get() = paused == true
+ private var timer: TimerInterface? = null
+
+ fun update(player: MediaPlayerEntity) {
+ paused = player.paused ?: true
+
+ if (paused == false) {
+ numPausedPings = 0
+ }
+ }
+
+ fun subscribe(callback: () -> Unit) {
+ timer = this.createTimer?.let { it() } ?: object : TimerInterface {
+ private var timer: Timer? = null
+
+ override fun schedule(task: TimerTask, delay: Long, period: Long) {
+ timer = Timer()
+ timer?.schedule(task, delay, period)
+ }
+
+ override fun cancel() {
+ timer?.cancel()
+ }
+ }
+
+ timer?.schedule(
+ object : TimerTask() {
+ override fun run() {
+ if (!isPaused || numPausedPings < maxPausedPings) {
+ if (isPaused) {
+ numPausedPings += 1
+ }
+ callback()
+ }
+ }
+ },
+ pingInterval * 1000L,
+ pingInterval * 1000L
+ )
+ }
+
+ fun end() {
+ timer?.cancel()
+ timer = null
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaSessionTracking.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaSessionTracking.kt
new file mode 100644
index 000000000..0a1235de3
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaSessionTracking.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.controller
+
+import com.snowplowanalytics.core.media.entity.MediaSessionEntity
+import com.snowplowanalytics.snowplow.event.Event
+import com.snowplowanalytics.snowplow.media.entity.MediaAdBreakEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
+import java.util.*
+
+class MediaSessionTracking(
+ id: String,
+ startedAt: Date? = null,
+ pingInterval: Int? = null,
+ dateGenerator: () -> Date = { Date() }
+) {
+ var session: MediaSessionEntity
+ var stats: MediaSessionTrackingStats
+
+ val entity: SelfDescribingJson
+ get() = session.entity(stats)
+
+ init {
+ session = MediaSessionEntity(id = id, startedAt = startedAt ?: Date(), pingInterval = pingInterval)
+ stats = MediaSessionTrackingStats(session = session, dateGenerator = dateGenerator)
+ }
+
+ fun update(event: Event?, player: MediaPlayerEntity, adBreak: MediaAdBreakEntity?) {
+ stats.update(event, player, adBreak)
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaSessionTrackingStats.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaSessionTrackingStats.kt
new file mode 100644
index 000000000..f047054d0
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaSessionTrackingStats.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.controller
+
+import com.snowplowanalytics.core.media.entity.MediaSessionEntity
+import com.snowplowanalytics.snowplow.event.Event
+import com.snowplowanalytics.snowplow.media.entity.MediaAdBreakEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaAdBreakType
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import com.snowplowanalytics.snowplow.media.event.*
+import java.util.Date
+import kotlin.time.Duration
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+
+private data class Log(
+ val time: Date,
+ val contentTime: Double,
+ val playbackRate: Double,
+ val paused: Boolean,
+ val muted: Boolean,
+ val linearAd: Boolean
+)
+
+class MediaSessionTrackingStats(
+ var session: MediaSessionEntity,
+ private val dateGenerator: () -> Date = { Date() }
+) {
+ private var lastAdUpdateAt: Date? = null
+ private var bufferingStartedAt: Date? = null
+ private var bufferingStartTime: Double? = null
+ private var playbackDurationWithPlaybackRate: Duration = Duration.ZERO
+ private var playedSeconds: MutableSet = mutableSetOf()
+ private var lastLog: Log? = null
+
+ val contentWatched: Duration
+ get() = playedSeconds.size.toDuration(DurationUnit.SECONDS)
+ var timeSpentAds: Duration = Duration.ZERO
+ private set
+ var timePlayed: Duration = Duration.ZERO
+ var timePlayedMuted: Duration = Duration.ZERO
+ var timePaused: Duration = Duration.ZERO
+ var timeBuffering: Duration = Duration.ZERO
+ val avgPlaybackRate: Double
+ get() = if (timePlayed > Duration.ZERO) {
+ playbackDurationWithPlaybackRate / timePlayed
+ } else {
+ 1.0
+ }
+ var adBreaks: Int = 0
+ var ads: Int = 0
+ var adsSkipped: Int = 0
+ var adsClicked: Int = 0
+
+ fun update(event: Event?, player: MediaPlayerEntity, adBreak: MediaAdBreakEntity? = null) {
+ val log = Log(
+ time = dateGenerator(),
+ contentTime = player.currentTime ?: 0.0,
+ playbackRate = player.playbackRate ?: 1.0,
+ paused = player.paused ?: true,
+ muted = player.muted ?: false,
+ linearAd = (adBreak?.breakType ?: MediaAdBreakType.Linear) == MediaAdBreakType.Linear
+ )
+
+ updateDurationStats(log)
+ updateAdStats(event, log)
+ updateBufferingStats(event, log)
+
+ lastLog = log
+ }
+
+ private fun updateDurationStats(log: Log) {
+ val wasPlayingAd = lastAdUpdateAt != null
+ val shouldCountStats = !wasPlayingAd || !log.linearAd
+
+ if (!shouldCountStats) {
+ return
+ }
+
+ lastLog?.let { lastLog ->
+ // add the time diff since last event to duration stats
+ val duration = timeDiff(lastLog.time, log.time)
+ if (lastLog.paused) {
+ timePaused += duration
+ } else {
+ timePlayed += duration
+ playbackDurationWithPlaybackRate += duration * lastLog.playbackRate
+
+ if (lastLog.muted) {
+ timePlayedMuted += duration
+ }
+
+ if (!log.paused && log.contentTime > lastLog.contentTime) {
+ for (i in lastLog.contentTime.toInt() until log.contentTime.toInt()) {
+ playedSeconds.add(i)
+ }
+ }
+ }
+
+ if (!log.paused) {
+ playedSeconds.add(log.contentTime.toInt())
+ }
+ }
+ }
+
+ private fun updateAdStats(event: Event?, log: Log) {
+ // count ad actions
+ when (event) {
+ is MediaAdBreakStartEvent -> {
+ adBreaks++
+ }
+ is MediaAdStartEvent -> {
+ ads++
+ }
+ is MediaAdSkipEvent -> {
+ adsSkipped++
+ }
+ is MediaAdClickEvent -> {
+ adsClicked++
+ }
+ }
+
+ // update ad playback duration
+ when (event) {
+ // ad start
+ is MediaAdStartEvent, is MediaAdResumeEvent -> {
+ if (lastAdUpdateAt == null) {
+ lastAdUpdateAt = log.time
+ }
+ }
+
+ // ad progress
+ is MediaAdClickEvent, is MediaAdFirstQuartileEvent, is MediaAdMidpointEvent, is MediaAdThirdQuartileEvent -> {
+ lastAdUpdateAt?.let { lastAdUpdateAt ->
+ timeSpentAds += timeDiff(lastAdUpdateAt, log.time)
+ }
+ lastAdUpdateAt = log.time
+ }
+
+ // ad end
+ is MediaAdCompleteEvent, is MediaAdSkipEvent, is MediaAdPauseEvent -> {
+ lastAdUpdateAt?.let { lastAdUpdateAt ->
+ timeSpentAds += timeDiff(lastAdUpdateAt, log.time)
+ }
+ lastAdUpdateAt = null
+ }
+ }
+ }
+
+ private fun updateBufferingStats(event: Event?, log: Log) {
+ if (event is MediaBufferStartEvent) {
+ bufferingStartedAt = log.time
+ bufferingStartTime = log.contentTime
+ } else {
+ val bufferingStartedAt = bufferingStartedAt ?: return
+ val bufferingStartTime = bufferingStartTime ?: return
+
+ if (
+ (log.contentTime != bufferingStartTime && !log.paused) ||
+ event is MediaBufferEndEvent ||
+ event is MediaPlayEvent
+ ) {
+ // Either the playback moved or BufferEnd or Play events were tracked
+ timeBuffering += timeDiff(bufferingStartedAt, log.time)
+ this.bufferingStartedAt = null
+ this.bufferingStartTime = null
+ } else {
+ // Still buffering, just update the ongoing duration
+ timeBuffering += timeDiff(bufferingStartedAt, log.time)
+ this.bufferingStartedAt = log.time
+ }
+ }
+ }
+
+ private fun timeDiff(since: Date, until: Date): Duration {
+ return (until.time - since.time).toDuration(DurationUnit.MILLISECONDS)
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaTrackingImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaTrackingImpl.kt
new file mode 100644
index 000000000..b9df49c84
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/controller/MediaTrackingImpl.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.controller
+
+import com.snowplowanalytics.core.media.event.MediaPercentProgressEvent
+import com.snowplowanalytics.core.media.event.MediaPingEvent
+import com.snowplowanalytics.core.media.event.MediaPlayerUpdatingEvent
+import com.snowplowanalytics.snowplow.controller.TrackerController
+import com.snowplowanalytics.snowplow.event.Event
+import com.snowplowanalytics.snowplow.media.controller.MediaTracking
+import com.snowplowanalytics.snowplow.media.entity.MediaAdBreakEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaAdEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import com.snowplowanalytics.snowplow.media.event.MediaSeekEndEvent
+import com.snowplowanalytics.snowplow.media.event.MediaSeekStartEvent
+import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
+import kotlin.reflect.KClass
+
+class MediaTrackingImpl (
+ override val id: String,
+ private val tracker: TrackerController,
+ player: MediaPlayerEntity? = null,
+ private var session: MediaSessionTracking? = null,
+ private var pingInterval: MediaPingInterval? = null,
+ private var boundaries: List? = null,
+ private var captureEvents: List>? = null,
+ private var customEntities: List? = null,
+) : MediaTracking {
+ private var player: MediaPlayerEntity = MediaPlayerEntity()
+ private val adTracking: MediaAdTracking = MediaAdTracking()
+ private val sentBoundaries: MutableList = mutableListOf()
+ private var seeking = false
+
+ private val entities: List
+ get() = listOfNotNull(
+ player.entity,
+ session?.entity,
+ ) + adTracking.entities + (customEntities ?: emptyList())
+
+ init {
+ player?.let { this.player.update(it) }
+
+ pingInterval?.subscribe { track(MediaPingEvent()) }
+ }
+
+ fun end() {
+ pingInterval?.end()
+ }
+
+ override fun update(
+ player: MediaPlayerEntity?,
+ ad: MediaAdEntity?,
+ adBreak: MediaAdBreakEntity?
+ ) {
+ updateAndTrack(null, player, ad, adBreak)
+ }
+
+ override fun track(
+ event: Event,
+ player: MediaPlayerEntity?,
+ ad: MediaAdEntity?,
+ adBreak: MediaAdBreakEntity?
+ ) {
+ updateAndTrack(event, player, ad, adBreak)
+ }
+
+ private fun updateAndTrack(
+ event: Event?,
+ player: MediaPlayerEntity?,
+ ad: MediaAdEntity?,
+ adBreak: MediaAdBreakEntity?
+ ) {
+ synchronized(this) {
+ // update state
+ player?.let { this.player.update(it) }
+ (event as? MediaPlayerUpdatingEvent)?.update(this.player)
+ adTracking.updateForThisEvent(
+ event = event,
+ player = this.player,
+ ad = ad,
+ adBreak = adBreak
+ )
+ session?.update(
+ event = event,
+ player = this.player,
+ adBreak = adBreak
+ )
+ pingInterval?.update(this.player)
+
+ // track events
+ event?.let { addEntitiesAndTrack(it) }
+ if (shouldSendPercentProgressEvent()) {
+ addEntitiesAndTrack(MediaPercentProgressEvent())
+ }
+
+ // update state for events after this one
+ adTracking.updateForNextEvent(event)
+ }
+ }
+
+ private fun addEntitiesAndTrack(event: Event) {
+ if (!shouldTrackEvent(event)) { return }
+
+ event.entities.addAll(entities)
+
+ tracker.track(event)
+ }
+
+ private fun shouldSendPercentProgressEvent(): Boolean {
+ if (player.paused ?: true) {
+ return false
+ }
+
+ val boundaries = boundaries ?: return false
+ val percentProgress = player.percentProgress ?: return false
+
+ val achievedBoundaries = boundaries.filter { it <= percentProgress }
+ if (achievedBoundaries.isEmpty()) { return false }
+
+ val boundary = achievedBoundaries.max()
+ if (sentBoundaries.contains(boundary)) { return false }
+
+ sentBoundaries.add(boundary)
+ return true
+ }
+
+ private fun shouldTrackEvent(event: Event): Boolean {
+ if (event is MediaSeekStartEvent) {
+ if (seeking) {
+ return false
+ }
+ seeking = true
+ } else if (event is MediaSeekEndEvent) {
+ seeking = false
+ }
+
+ val captureEvents = captureEvents ?: return true
+ return captureEvents.contains(event::class)
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/entity/MediaSessionEntity.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/entity/MediaSessionEntity.kt
new file mode 100644
index 000000000..64f811965
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/entity/MediaSessionEntity.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.entity
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.core.media.controller.MediaSessionTrackingStats
+import com.snowplowanalytics.core.utils.Util.getDateTimeFromDate
+import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
+import java.util.Date
+import kotlin.time.Duration
+import kotlin.time.DurationUnit
+
+class MediaSessionEntity(
+ var id: String,
+ var startedAt: Date = Date(),
+ var pingInterval: Int? = null,
+) {
+ fun entity(stats: MediaSessionTrackingStats): SelfDescribingJson {
+ return SelfDescribingJson(
+ schema = MediaSchemata.sessionSchema,
+ data = mapOf(
+ "mediaSessionId" to id,
+ "startedAt" to getDateTimeFromDate(startedAt),
+ "pingInterval" to pingInterval,
+ "timePlayed" to roundDuration(stats.timePlayed),
+ "timePaused" to roundDuration(stats.timePaused),
+ "timePlayedMuted" to roundDuration(stats.timePlayedMuted),
+ "timeSpentAds" to roundDuration(stats.timeSpentAds),
+ "timeBuffering" to roundDuration(stats.timeBuffering),
+ "ads" to if (stats.ads > 0) { stats.ads } else { null },
+ "adBreaks" to if (stats.adBreaks > 0) { stats.adBreaks } else { null },
+ "adsSkipped" to if (stats.adsSkipped > 0) { stats.adsSkipped } else { null },
+ "adsClicked" to if (stats.adsClicked > 0) { stats.adsClicked } else { null },
+ "avgPlaybackRate" to if (stats.avgPlaybackRate != 1.0) { roundStat(stats.avgPlaybackRate) } else { null },
+ "contentWatched" to roundDuration(stats.contentWatched),
+ ).filterValues { it != null }
+ )
+ }
+
+ private fun roundStat(stat: Double?): Double? {
+ return stat?.let { return (it * 1000).toInt() / 1000.0 }
+ }
+
+ private fun roundDuration(duration: Duration): Double? {
+ if (duration > Duration.ZERO) {
+ return roundStat(duration.toDouble(DurationUnit.SECONDS))
+ } else {
+ return null
+ }
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/event/MediaPercentProgressEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/event/MediaPercentProgressEvent.kt
new file mode 100644
index 000000000..0e94d0fdb
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/event/MediaPercentProgressEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when a percentage boundary set in the `boundaries` list in `MediaTrackingConfiguration` is reached.
+ */
+class MediaPercentProgressEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("percent_progress")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/event/MediaPingEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/event/MediaPingEvent.kt
new file mode 100644
index 000000000..2e1d2be55
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/event/MediaPingEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired periodically during main content playback, regardless of other API events that have been sent
+ */
+class MediaPingEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ping")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/event/MediaPlayerUpdatingEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/event/MediaPlayerUpdatingEvent.kt
new file mode 100644
index 000000000..d4bb11b8e
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/media/event/MediaPlayerUpdatingEvent.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.core.media.event
+
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+interface MediaPlayerUpdatingEvent {
+ /**
+ * Updates event properties based on the player entity but also updates the player properties based on the event.
+ */
+ fun update(player: MediaPlayerEntity)
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/FetchedConfigurationBundle.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationBundle.kt
similarity index 84%
rename from snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/FetchedConfigurationBundle.kt
rename to snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationBundle.kt
index e37cdf778..4cacd73e4 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/FetchedConfigurationBundle.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationBundle.kt
@@ -17,7 +17,7 @@ import com.snowplowanalytics.snowplow.configuration.Configuration
import com.snowplowanalytics.snowplow.configuration.ConfigurationBundle
import org.json.JSONObject
-class FetchedConfigurationBundle : Configuration {
+class RemoteConfigurationBundle : Configuration {
var schema: String
var configurationVersion: Int
var configurationBundle: List
@@ -43,9 +43,17 @@ class FetchedConfigurationBundle : Configuration {
configurationBundle = tempBundle.toList()
}
+ fun updateSourceConfig(sourceRemoteBundle: RemoteConfigurationBundle) {
+ for (bundle in configurationBundle) {
+ sourceRemoteBundle.configurationBundle.find { it.namespace == bundle.namespace }?.let {
+ bundle.updateSourceConfig(it)
+ }
+ }
+ }
+
// Copyable
override fun copy(): Configuration {
- val copy = FetchedConfigurationBundle(schema)
+ val copy = RemoteConfigurationBundle(schema)
copy.configurationVersion = configurationVersion
val tempBundle = ArrayList()
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationCache.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationCache.kt
similarity index 87%
rename from snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationCache.kt
rename to snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationCache.kt
index eb6e023f9..036e3a901 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationCache.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationCache.kt
@@ -16,12 +16,12 @@ import android.content.Context
import com.snowplowanalytics.snowplow.configuration.RemoteConfiguration
import java.io.*
-class ConfigurationCache(private val remoteConfiguration: RemoteConfiguration) {
+class RemoteConfigurationCache(private val remoteConfiguration: RemoteConfiguration) {
private var cacheFilePath: String? = null
- private var configuration: FetchedConfigurationBundle? = null
+ private var configuration: RemoteConfigurationBundle? = null
@Synchronized
- fun readCache(context: Context): FetchedConfigurationBundle? {
+ fun readCache(context: Context): RemoteConfigurationBundle? {
if (configuration != null) { return configuration }
loadCache(context)
@@ -29,7 +29,7 @@ class ConfigurationCache(private val remoteConfiguration: RemoteConfiguration) {
}
@Synchronized
- fun writeCache(context: Context, configuration: FetchedConfigurationBundle) {
+ fun writeCache(context: Context, configuration: RemoteConfigurationBundle) {
this.configuration = configuration
storeCache(context, configuration)
}
@@ -61,7 +61,7 @@ class ConfigurationCache(private val remoteConfiguration: RemoteConfiguration) {
try {
val fileIn = FileInputStream(path)
objectIn = ObjectInputStream(fileIn)
- configuration = objectIn.readObject() as? FetchedConfigurationBundle
+ configuration = objectIn.readObject() as? RemoteConfigurationBundle
} catch (e: FileNotFoundException) {
// TODO log exception
} catch (e: IOException) {
@@ -78,7 +78,7 @@ class ConfigurationCache(private val remoteConfiguration: RemoteConfiguration) {
}
}
- private fun storeCache(context: Context, configuration: FetchedConfigurationBundle) {
+ private fun storeCache(context: Context, configuration: RemoteConfigurationBundle) {
val path = getCachePath(context)
var objectOut: ObjectOutputStream? = null
try {
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationFetcher.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationFetcher.kt
similarity index 91%
rename from snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationFetcher.kt
rename to snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationFetcher.kt
index c1aac6869..efc39d5fd 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationFetcher.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationFetcher.kt
@@ -31,12 +31,12 @@ import java.io.IOException
import java.util.concurrent.TimeUnit
-class ConfigurationFetcher(
+class RemoteConfigurationFetcher(
context: Context,
private val remoteConfiguration: RemoteConfiguration,
- private val onFetchCallback: Consumer
+ private val onFetchCallback: Consumer
) {
- private val TAG = ConfigurationFetcher::class.java.simpleName
+ private val TAG = RemoteConfigurationFetcher::class.java.simpleName
init {
execute(getRunnable(context)) { t: Throwable? -> exceptionHandler(t) }
@@ -78,11 +78,11 @@ class ConfigurationFetcher(
private fun resolveRequest(
context: Context,
responseBody: ResponseBody,
- onFetchCallback: Consumer
+ onFetchCallback: Consumer
) {
val data = responseBody.string()
val jsonObject = JSONObject(data)
- val bundle = FetchedConfigurationBundle(context, jsonObject)
+ val bundle = RemoteConfigurationBundle(context, jsonObject)
onFetchCallback.accept(bundle)
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationProvider.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationProvider.kt
similarity index 72%
rename from snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationProvider.kt
rename to snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationProvider.kt
index fb0902b01..a9652defc 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/ConfigurationProvider.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/remoteconfiguration/RemoteConfigurationProvider.kt
@@ -24,19 +24,19 @@ import com.snowplowanalytics.snowplow.configuration.RemoteConfiguration
* This class fetch a configuration from a remote source otherwise it provides a cached configuration.
* It can manage multiple sources and multiple caches.
*/
-class ConfigurationProvider @JvmOverloads constructor(
+class RemoteConfigurationProvider @JvmOverloads constructor(
private val remoteConfiguration: RemoteConfiguration,
defaultBundles: List? = null,
defaultBundleVersion: Int = Int.MIN_VALUE
) {
- private val cache: ConfigurationCache = ConfigurationCache(remoteConfiguration)
- private var fetcher: ConfigurationFetcher? = null
- private var defaultBundle: FetchedConfigurationBundle? = null
- private var cacheBundle: FetchedConfigurationBundle? = null
+ private val cache: RemoteConfigurationCache = RemoteConfigurationCache(remoteConfiguration)
+ private var fetcher: RemoteConfigurationFetcher? = null
+ private var defaultBundle: RemoteConfigurationBundle? = null
+ private var cacheBundle: RemoteConfigurationBundle? = null
init {
if (defaultBundles != null) {
- val bundle = FetchedConfigurationBundle("1.0")
+ val bundle = RemoteConfigurationBundle("1.0")
bundle.configurationVersion = defaultBundleVersion
bundle.configurationBundle = defaultBundles
defaultBundle = bundle
@@ -47,34 +47,36 @@ class ConfigurationProvider @JvmOverloads constructor(
fun retrieveConfiguration(
context: Context,
onlyRemote: Boolean,
- onFetchCallback: Consumer>
+ onFetchCallback: Consumer>
) {
if (!onlyRemote) {
if (cacheBundle == null) {
cacheBundle = cache.readCache(context)
}
if (cacheBundle != null) {
+ defaultBundle?.let { cacheBundle?.updateSourceConfig(it) }
onFetchCallback.accept(Pair(cacheBundle, ConfigurationState.CACHED))
} else if (defaultBundle != null) {
onFetchCallback.accept(Pair(defaultBundle, ConfigurationState.DEFAULT))
}
}
- fetcher = ConfigurationFetcher(
+ fetcher = RemoteConfigurationFetcher(
context,
remoteConfiguration,
- object : Consumer {
- override fun accept(fetchedConfigurationBundle: FetchedConfigurationBundle) {
- if (!schemaCompatibility(fetchedConfigurationBundle.schema)) {
+ object : Consumer {
+ override fun accept(bundle: RemoteConfigurationBundle) {
+ if (!schemaCompatibility(bundle.schema)) {
return
}
synchronized(this) {
- val isNewer = (cacheBundle ?: defaultBundle)?.let { it.configurationVersion < fetchedConfigurationBundle.configurationVersion } ?: true
+ val isNewer = (cacheBundle ?: defaultBundle)?.let { it.configurationVersion < bundle.configurationVersion } ?: true
if (isNewer) {
- cache.writeCache(context, fetchedConfigurationBundle)
- cacheBundle = fetchedConfigurationBundle
+ defaultBundle?.let { bundle.updateSourceConfig(it) }
+ cache.writeCache(context, bundle)
+ cacheBundle = bundle
onFetchCallback.accept(
Pair(
- fetchedConfigurationBundle,
+ bundle,
ConfigurationState.FETCHED
)
)
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionConfigurationUpdate.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionConfigurationUpdate.kt
deleted file mode 100644
index 25006e664..000000000
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionConfigurationUpdate.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
- *
- * This program is licensed to you under the Apache License Version 2.0,
- * and you may not use this file except in compliance with the Apache License Version 2.0.
- * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the Apache License Version 2.0 is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
- */
-package com.snowplowanalytics.core.session
-
-import androidx.core.util.Consumer
-import com.snowplowanalytics.snowplow.configuration.SessionConfiguration
-import com.snowplowanalytics.snowplow.tracker.SessionState
-import com.snowplowanalytics.snowplow.util.TimeMeasure
-import java.util.concurrent.TimeUnit
-
-class SessionConfigurationUpdate @JvmOverloads constructor(
- foregroundTimeout: TimeMeasure = TimeMeasure(30, TimeUnit.MINUTES),
- backgroundTimeout: TimeMeasure = TimeMeasure(30, TimeUnit.MINUTES)
-) : SessionConfiguration(foregroundTimeout, backgroundTimeout) {
-
- var sourceConfig: SessionConfiguration? = null
- var isPaused = false
- private var foregroundTimeoutUpdated = false
- private var backgroundTimeoutUpdated = false
-
- override var foregroundTimeout: TimeMeasure
- get() = if (sourceConfig == null || foregroundTimeoutUpdated) super.foregroundTimeout else sourceConfig!!.foregroundTimeout
- set(value) {
- super.foregroundTimeout = value
- foregroundTimeoutUpdated = true
- }
-
- override var backgroundTimeout: TimeMeasure
- get() = if (sourceConfig == null || backgroundTimeoutUpdated) super.backgroundTimeout else sourceConfig!!.backgroundTimeout
- set(value) {
- super.backgroundTimeout = value
- backgroundTimeoutUpdated = true
- }
-
- override var onSessionUpdate: Consumer?
- get() = if (sourceConfig == null) null else sourceConfig!!.onSessionUpdate
- set(value) {
- // Can't update this
- }
-}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionControllerImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionControllerImpl.kt
index 9854a981f..ea2857564 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionControllerImpl.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/session/SessionControllerImpl.kt
@@ -18,6 +18,7 @@ import com.snowplowanalytics.core.Controller
import com.snowplowanalytics.core.tracker.Logger
import com.snowplowanalytics.core.tracker.ServiceProviderInterface
import com.snowplowanalytics.core.tracker.Tracker
+import com.snowplowanalytics.snowplow.configuration.SessionConfiguration
import com.snowplowanalytics.snowplow.controller.SessionController
import com.snowplowanalytics.snowplow.tracker.SessionState
import com.snowplowanalytics.snowplow.util.TimeMeasure
@@ -164,6 +165,7 @@ class SessionControllerImpl // Constructors
Logger.track(TAG, "Attempt to access SessionController fields when disabled")
return
}
+ dirtyConfig.onSessionUpdate = onSessionUpdate
session.onSessionUpdate = onSessionUpdate
}
@@ -176,6 +178,6 @@ class SessionControllerImpl // Constructors
get() = serviceProvider.getOrMakeTracker()
private val session: Session?
get() = serviceProvider.getOrMakeTracker().session
- private val dirtyConfig: SessionConfigurationUpdate
- get() = serviceProvider.sessionConfigurationUpdate
+ private val dirtyConfig: SessionConfiguration
+ get() = serviceProvider.sessionConfiguration
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/PlatformContext.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/PlatformContext.kt
index 00b7e411e..8dbfd817e 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/PlatformContext.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/PlatformContext.kt
@@ -124,7 +124,7 @@ class PlatformContext(
}
// Language
if (shouldTrack(PlatformContextProperty.LANGUAGE)) {
- addToMap(Parameters.MOBILE_LANGUAGE, deviceInfoMonitor.language, pairs)
+ addToMap(Parameters.MOBILE_LANGUAGE, deviceInfoMonitor.language?.take(8), pairs)
}
setEphemeralPlatformDict()
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt
index 5f9295056..57b69a620 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProvider.kt
@@ -16,12 +16,12 @@ import android.content.Context
import androidx.annotation.RestrictTo
import com.snowplowanalytics.core.emitter.*
import com.snowplowanalytics.core.gdpr.Gdpr
-import com.snowplowanalytics.core.gdpr.GdprConfigurationUpdate
import com.snowplowanalytics.core.gdpr.GdprControllerImpl
import com.snowplowanalytics.core.globalcontexts.GlobalContextsControllerImpl
-import com.snowplowanalytics.core.session.SessionConfigurationUpdate
+import com.snowplowanalytics.core.media.controller.MediaControllerImpl
import com.snowplowanalytics.core.session.SessionControllerImpl
import com.snowplowanalytics.snowplow.configuration.*
+import com.snowplowanalytics.snowplow.media.controller.MediaController
import java.util.concurrent.TimeUnit
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -32,7 +32,6 @@ class ServiceProvider(
configurations: List
) : ServiceProviderInterface {
private val context: Context
- private val appId: String
override val isTrackerInitialized: Boolean
get() = tracker != null
@@ -51,39 +50,36 @@ class ServiceProvider(
override val pluginsController: PluginsControllerImpl by lazy {
PluginsControllerImpl(this)
}
+ override val mediaController: MediaController by lazy {
+ MediaControllerImpl(this)
+ }
- // Original configurations
+ // Configurations
+ override lateinit var trackerConfiguration: TrackerConfiguration
+ override lateinit var networkConfiguration: NetworkConfiguration
+ override lateinit var subjectConfiguration: SubjectConfiguration
+ override lateinit var emitterConfiguration: EmitterConfiguration
+ override lateinit var sessionConfiguration: SessionConfiguration
+ override lateinit var gdprConfiguration: GdprConfiguration
override var pluginConfigurations: MutableList = ArrayList()
private set
- // Configuration updates
- override lateinit var trackerConfigurationUpdate: TrackerConfigurationUpdate
- override lateinit var networkConfigurationUpdate: NetworkConfigurationUpdate
- override lateinit var subjectConfigurationUpdate: SubjectConfigurationUpdate
- override lateinit var emitterConfigurationUpdate: EmitterConfigurationUpdate
- override lateinit var sessionConfigurationUpdate: SessionConfigurationUpdate
- override lateinit var gdprConfigurationUpdate: GdprConfigurationUpdate
-
init {
// Initialization
this.context = context
- appId = context.packageName
-
+
// Reset configurationUpdates
- trackerConfigurationUpdate = TrackerConfigurationUpdate(appId)
- networkConfigurationUpdate = NetworkConfigurationUpdate()
- subjectConfigurationUpdate = SubjectConfigurationUpdate()
- emitterConfigurationUpdate = EmitterConfigurationUpdate()
- sessionConfigurationUpdate = SessionConfigurationUpdate()
- gdprConfigurationUpdate = GdprConfigurationUpdate()
+ trackerConfiguration = TrackerConfiguration()
+ this.networkConfiguration = NetworkConfiguration()
+ subjectConfiguration = SubjectConfiguration()
+ emitterConfiguration = EmitterConfiguration()
+ sessionConfiguration = SessionConfiguration()
+ gdprConfiguration = GdprConfiguration()
// Process configurations
- networkConfigurationUpdate.sourceConfig = networkConfiguration
+ this.networkConfiguration.sourceConfig = networkConfiguration
processConfigurations(configurations)
-
- if (trackerConfigurationUpdate.sourceConfig == null) {
- trackerConfigurationUpdate.sourceConfig = TrackerConfiguration(appId)
- }
+
getOrMakeTracker() // Build tracker to initialize NotificationCenter receivers
}
@@ -106,31 +102,33 @@ class ServiceProvider(
// Private methods
private fun processConfigurations(configurations: List) {
for (configuration in configurations) {
- if (configuration is NetworkConfiguration) {
- networkConfigurationUpdate.sourceConfig = configuration
- }
- else if (configuration is TrackerConfiguration) {
- trackerConfigurationUpdate.sourceConfig = configuration
- }
- else if (configuration is SubjectConfiguration) {
- subjectConfigurationUpdate.sourceConfig = configuration
- }
- else if (configuration is SessionConfiguration) {
- sessionConfigurationUpdate.sourceConfig = configuration
- }
- else if (configuration is EmitterConfiguration) {
- emitterConfigurationUpdate.sourceConfig = configuration
- }
- else if (configuration is GdprConfiguration) {
- gdprConfigurationUpdate.sourceConfig = configuration
- }
- else if (configuration is GlobalContextsConfiguration) {
- for (plugin in configuration.toPluginConfigurations()) {
- pluginConfigurations.add(plugin)
+ when (configuration) {
+ is NetworkConfiguration -> {
+ this.networkConfiguration.sourceConfig = configuration
+ }
+ is TrackerConfiguration -> {
+ trackerConfiguration.sourceConfig = configuration
+ }
+ is SubjectConfiguration -> {
+ subjectConfiguration.sourceConfig = configuration
+ }
+ is SessionConfiguration -> {
+ sessionConfiguration.sourceConfig = configuration
+ }
+ is EmitterConfiguration -> {
+ emitterConfiguration.sourceConfig = configuration
+ }
+ is GdprConfiguration -> {
+ gdprConfiguration.sourceConfig = configuration
+ }
+ is GlobalContextsConfiguration -> {
+ for (plugin in configuration.toPluginConfigurations()) {
+ pluginConfigurations.add(plugin)
+ }
+ }
+ is PluginIdentifiable -> {
+ pluginConfigurations.add(configuration)
}
- }
- else if (configuration is PluginIdentifiable) {
- pluginConfigurations.add(configuration)
}
}
}
@@ -158,20 +156,20 @@ class ServiceProvider(
private fun resetConfigurationUpdates() {
// Don't reset networkConfiguration as it's needed in case it's not passed in the new configurations.
// Set a default trackerConfiguration to reset to default if not passed.
- trackerConfigurationUpdate.sourceConfig = TrackerConfiguration(appId)
- subjectConfigurationUpdate.sourceConfig = null
- emitterConfigurationUpdate.sourceConfig = null
- sessionConfigurationUpdate.sourceConfig = null
- gdprConfigurationUpdate.sourceConfig = null
+ trackerConfiguration.sourceConfig = null
+ subjectConfiguration.sourceConfig = null
+ emitterConfiguration.sourceConfig = null
+ sessionConfiguration.sourceConfig = null
+ gdprConfiguration.sourceConfig = null
}
private fun initializeConfigurationUpdates() {
- networkConfigurationUpdate = NetworkConfigurationUpdate()
- trackerConfigurationUpdate = TrackerConfigurationUpdate(appId)
- emitterConfigurationUpdate = EmitterConfigurationUpdate()
- subjectConfigurationUpdate = SubjectConfigurationUpdate()
- sessionConfigurationUpdate = SessionConfigurationUpdate()
- gdprConfigurationUpdate = GdprConfigurationUpdate()
+ this.networkConfiguration = NetworkConfiguration()
+ trackerConfiguration = TrackerConfiguration()
+ emitterConfiguration = EmitterConfiguration()
+ subjectConfiguration = SubjectConfiguration()
+ sessionConfiguration = SessionConfiguration()
+ gdprConfiguration = GdprConfiguration()
}
// Getters
@@ -217,36 +215,35 @@ class ServiceProvider(
// Factories
private fun makeSubject(): Subject {
- return Subject(context, subjectConfigurationUpdate)
+ return Subject(context, subjectConfiguration)
}
private fun makeEmitter(): Emitter {
- val networkConfig = networkConfigurationUpdate
- val emitterConfig = emitterConfigurationUpdate
- val endpoint = networkConfig.endpoint ?: ""
+ val endpoint = networkConfiguration.endpoint ?: ""
val builder = { emitter: Emitter ->
- networkConfig.method?.let { emitter.httpMethod = it }
- networkConfig.protocol?.let { emitter.requestSecurity = it }
+ emitter.httpMethod = networkConfiguration.method
+ networkConfiguration.protocol?.let { emitter.requestSecurity = it }
- emitter.networkConnection = networkConfig.networkConnection
- emitter.customPostPath = networkConfig.customPostPath
- emitter.client = networkConfig.okHttpClient
- emitter.cookieJar = networkConfig.okHttpCookieJar
- emitter.emitTimeout = networkConfig.timeout
- emitter.sendLimit = emitterConfig.emitRange
- emitter.bufferOption = emitterConfig.bufferOption
- emitter.eventStore = emitterConfig.eventStore
- emitter.byteLimitPost = emitterConfig.byteLimitPost
- emitter.byteLimitGet = emitterConfig.byteLimitGet
- emitter.threadPoolSize = emitterConfig.threadPoolSize
- emitter.requestCallback = emitterConfig.requestCallback
- emitter.customRetryForStatusCodes = emitterConfig.customRetryForStatusCodes
- emitter.serverAnonymisation = emitterConfig.serverAnonymisation
+ emitter.networkConnection = networkConfiguration.networkConnection
+ emitter.customPostPath = networkConfiguration.customPostPath
+ emitter.client = networkConfiguration.okHttpClient
+ emitter.cookieJar = networkConfiguration.okHttpCookieJar
+ emitter.emitTimeout = networkConfiguration.timeout
+ emitter.sendLimit = emitterConfiguration.emitRange
+ emitter.bufferOption = emitterConfiguration.bufferOption
+ emitter.eventStore = emitterConfiguration.eventStore
+ emitter.byteLimitPost = emitterConfiguration.byteLimitPost
+ emitter.byteLimitGet = emitterConfiguration.byteLimitGet
+ emitter.threadPoolSize = emitterConfiguration.threadPoolSize
+ emitter.requestCallback = emitterConfiguration.requestCallback
+ emitter.customRetryForStatusCodes = emitterConfiguration.customRetryForStatusCodes
+ emitter.serverAnonymisation = emitterConfiguration.serverAnonymisation
+ emitter.requestHeaders = networkConfiguration.requestHeaders
}
val emitter = Emitter(context, endpoint, builder)
- if (emitterConfig.isPaused) {
+ if (emitterConfiguration.isPaused) {
emitter.pauseEmit()
}
return emitter
@@ -255,39 +252,36 @@ class ServiceProvider(
private fun makeTracker(): Tracker {
val emitter = getOrMakeEmitter()
val subject = getOrMakeSubject()
- val trackerConfig = trackerConfigurationUpdate
- val sessionConfig = sessionConfigurationUpdate
- val gdprConfig = gdprConfigurationUpdate
val builder = { tracker: Tracker ->
tracker.subject = subject
- tracker.trackerVersionSuffix = trackerConfig.trackerVersionSuffix
- tracker.base64Encoded = trackerConfig.base64encoding
- tracker.platform = trackerConfig.devicePlatform
- tracker.logLevel = trackerConfig.logLevel
- tracker.loggerDelegate = trackerConfig.loggerDelegate
- tracker.sessionContext = trackerConfig.sessionContext
- tracker.applicationContext = trackerConfig.applicationContext
- tracker.platformContextEnabled = trackerConfig.platformContext
- tracker.geoLocationContext = trackerConfig.geoLocationContext
- tracker.deepLinkContext = trackerConfig.deepLinkContext
- tracker.screenContext = trackerConfig.screenContext
- tracker.screenViewAutotracking = trackerConfig.screenViewAutotracking
- tracker.lifecycleAutotracking = trackerConfig.lifecycleAutotracking
- tracker.installAutotracking = trackerConfigurationUpdate.installAutotracking
- tracker.exceptionAutotracking = trackerConfig.exceptionAutotracking
- tracker.diagnosticAutotracking = trackerConfig.diagnosticAutotracking
- tracker.userAnonymisation = trackerConfig.userAnonymisation
- tracker.trackerVersionSuffix = trackerConfig.trackerVersionSuffix
-
- gdprConfig.sourceConfig?.let { tracker.gdprContext = Gdpr(
- gdprConfig.basisForProcessing,
- gdprConfig.documentId,
- gdprConfig.documentVersion,
- gdprConfig.documentDescription) }
-
- tracker.backgroundTimeout = sessionConfig.backgroundTimeout.convert(TimeUnit.SECONDS)
- tracker.foregroundTimeout = sessionConfig.foregroundTimeout.convert(TimeUnit.SECONDS)
+ tracker.trackerVersionSuffix = trackerConfiguration.trackerVersionSuffix
+ tracker.base64Encoded = trackerConfiguration.base64encoding
+ tracker.platform = trackerConfiguration.devicePlatform
+ tracker.logLevel = trackerConfiguration.logLevel
+ tracker.loggerDelegate = trackerConfiguration.loggerDelegate
+ tracker.sessionContext = trackerConfiguration.sessionContext
+ tracker.applicationContext = trackerConfiguration.applicationContext
+ tracker.platformContextEnabled = trackerConfiguration.platformContext
+ tracker.geoLocationContext = trackerConfiguration.geoLocationContext
+ tracker.deepLinkContext = trackerConfiguration.deepLinkContext
+ tracker.screenContext = trackerConfiguration.screenContext
+ tracker.screenViewAutotracking = trackerConfiguration.screenViewAutotracking
+ tracker.lifecycleAutotracking = trackerConfiguration.lifecycleAutotracking
+ tracker.installAutotracking = trackerConfiguration.installAutotracking
+ tracker.exceptionAutotracking = trackerConfiguration.exceptionAutotracking
+ tracker.diagnosticAutotracking = trackerConfiguration.diagnosticAutotracking
+ tracker.userAnonymisation = trackerConfiguration.userAnonymisation
+ tracker.trackerVersionSuffix = trackerConfiguration.trackerVersionSuffix
+
+ gdprConfiguration.sourceConfig?.let { tracker.gdprContext = Gdpr(
+ basisForProcessing = it.basisForProcessing,
+ documentId = it.documentId,
+ documentVersion = it.documentVersion,
+ documentDescription = it.documentDescription) }
+
+ tracker.backgroundTimeout = sessionConfiguration.backgroundTimeout.convert(TimeUnit.SECONDS)
+ tracker.foregroundTimeout = sessionConfiguration.foregroundTimeout.convert(TimeUnit.SECONDS)
for (plugin in pluginConfigurations) {
tracker.addOrReplaceStateMachine(plugin.toStateMachine())
@@ -297,21 +291,21 @@ class ServiceProvider(
val tracker = Tracker(
emitter,
namespace,
- trackerConfig.appId,
- trackerConfig.platformContextProperties,
+ trackerConfiguration.appId,
+ trackerConfiguration.platformContextProperties,
context,
builder
)
- if (trackerConfigurationUpdate.isPaused) {
+ if (trackerConfiguration.isPaused) {
tracker.pauseEventTracking()
}
- if (sessionConfigurationUpdate.isPaused) {
+ if (sessionConfiguration.isPaused) {
tracker.pauseSessionChecking()
}
val session = tracker.session
if (session != null) {
- val onSessionUpdate = sessionConfig.onSessionUpdate
+ val onSessionUpdate = sessionConfiguration.onSessionUpdate
if (onSessionUpdate != null) {
session.onSessionUpdate = onSessionUpdate
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProviderInterface.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProviderInterface.kt
index 03ce4d945..233b5f752 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProviderInterface.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/ServiceProviderInterface.kt
@@ -13,12 +13,12 @@
package com.snowplowanalytics.core.tracker
import com.snowplowanalytics.core.emitter.*
-import com.snowplowanalytics.core.gdpr.GdprConfigurationUpdate
import com.snowplowanalytics.core.gdpr.GdprControllerImpl
import com.snowplowanalytics.core.globalcontexts.GlobalContextsControllerImpl
-import com.snowplowanalytics.core.session.SessionConfigurationUpdate
import com.snowplowanalytics.core.session.SessionControllerImpl
import com.snowplowanalytics.snowplow.configuration.PluginIdentifiable
+import com.snowplowanalytics.snowplow.media.controller.MediaController
+import com.snowplowanalytics.snowplow.configuration.*
interface ServiceProviderInterface {
val namespace: String
@@ -38,14 +38,15 @@ interface ServiceProviderInterface {
fun getOrMakeSubjectController(): SubjectControllerImpl
fun getOrMakeSessionController(): SessionControllerImpl
val pluginsController: PluginsControllerImpl
+ val mediaController: MediaController
// Configuration Updates
- val trackerConfigurationUpdate: TrackerConfigurationUpdate
- val networkConfigurationUpdate: NetworkConfigurationUpdate
- val subjectConfigurationUpdate: SubjectConfigurationUpdate
- val emitterConfigurationUpdate: EmitterConfigurationUpdate
- val sessionConfigurationUpdate: SessionConfigurationUpdate
- val gdprConfigurationUpdate: GdprConfigurationUpdate
+ val trackerConfiguration: TrackerConfiguration
+ val networkConfiguration: NetworkConfiguration
+ val subjectConfiguration: SubjectConfiguration
+ val emitterConfiguration: EmitterConfiguration
+ val sessionConfiguration: SessionConfiguration
+ val gdprConfiguration: GdprConfiguration
// Plugins
val pluginConfigurations: List
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/SubjectConfigurationUpdate.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/SubjectConfigurationUpdate.kt
deleted file mode 100644
index b67788c71..000000000
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/SubjectConfigurationUpdate.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
- *
- * This program is licensed to you under the Apache License Version 2.0,
- * and you may not use this file except in compliance with the Apache License Version 2.0.
- * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the Apache License Version 2.0 is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
- */
-package com.snowplowanalytics.core.tracker
-
-import com.snowplowanalytics.snowplow.configuration.SubjectConfiguration
-import com.snowplowanalytics.snowplow.util.Size
-
-class SubjectConfigurationUpdate : SubjectConfiguration() {
- var sourceConfig: SubjectConfiguration? = null
- private var userIdUpdated = false
- private var networkUserIdUpdated = false
- private var domainUserIdUpdated = false
- private var useragentUpdated = false
- private var ipAddressUpdated = false
- private var timezoneUpdated = false
- private var languageUpdated = false
- private var screenResolutionUpdated = false
- private var screenViewPortUpdated = false
- private var colorDepthUpdated = false
-
- override var userId: String?
- get() = if (sourceConfig == null || userIdUpdated) super.userId else sourceConfig!!.userId
- set(value) {
- super.userId = value
- userIdUpdated = true
- }
-
- override var networkUserId: String?
- get() = if (sourceConfig == null || networkUserIdUpdated) super.networkUserId else sourceConfig!!.networkUserId
- set(value) {
- super.networkUserId = value
- networkUserIdUpdated = true
- }
-
- override var domainUserId: String?
- get() = if (sourceConfig == null || domainUserIdUpdated) super.domainUserId else sourceConfig!!.domainUserId
- set(value) {
- super.domainUserId = value
- domainUserIdUpdated = true
- }
-
- override var useragent: String?
- get() = if (sourceConfig == null || useragentUpdated) super.useragent else sourceConfig!!.useragent
- set(value) {
- super.useragent = value
- useragentUpdated = true
- }
-
- override var ipAddress: String?
- get() = if (sourceConfig == null || ipAddressUpdated) super.ipAddress else sourceConfig!!.ipAddress
- set(value) {
- super.ipAddress = value
- ipAddressUpdated = true
- }
-
- override var timezone: String?
- get() = if (sourceConfig == null || timezoneUpdated) super.timezone else sourceConfig!!.timezone
- set(value) {
- super.timezone = value
- timezoneUpdated = true
- }
-
- override var language: String?
- get() = if (sourceConfig == null || languageUpdated) super.language else sourceConfig!!.language
- set(value) {
- super.language = value
- languageUpdated = true
- }
-
- override var screenResolution: Size?
- get() = if (sourceConfig == null || screenResolutionUpdated) super.screenResolution else sourceConfig!!.screenResolution
- set(value) {
- super.screenResolution = value
- screenResolutionUpdated = true
- }
-
- override var screenViewPort: Size?
- get() = if (sourceConfig == null || screenViewPortUpdated) super.screenViewPort else sourceConfig!!.screenViewPort
- set(value) {
- super.screenViewPort = value
- screenViewPortUpdated = true
- }
-
- override var colorDepth: Int?
- get() = if (sourceConfig == null || colorDepthUpdated) super.colorDepth else sourceConfig!!.colorDepth
- set(value) {
- super.colorDepth = value
- colorDepthUpdated = true
- }
-}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/SubjectControllerImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/SubjectControllerImpl.kt
index 550e78ffc..4485bba4a 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/SubjectControllerImpl.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/SubjectControllerImpl.kt
@@ -14,6 +14,7 @@ package com.snowplowanalytics.core.tracker
import androidx.annotation.RestrictTo
import com.snowplowanalytics.core.Controller
+import com.snowplowanalytics.snowplow.configuration.SubjectConfiguration
import com.snowplowanalytics.snowplow.controller.SubjectController
import com.snowplowanalytics.snowplow.util.Size
@@ -94,6 +95,6 @@ class SubjectControllerImpl // Constructors
// Private methods
private val subject: Subject
get() = serviceProvider.getOrMakeSubject()
- private val dirtyConfig: SubjectConfigurationUpdate
- get() = serviceProvider.subjectConfigurationUpdate
+ private val dirtyConfig: SubjectConfiguration
+ get() = serviceProvider.subjectConfiguration
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt
index 0f4ceea6f..3db18d58c 100755
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/Tracker.kt
@@ -31,6 +31,7 @@ import com.snowplowanalytics.core.utils.NotificationCenter.addObserver
import com.snowplowanalytics.core.utils.NotificationCenter.removeObserver
import com.snowplowanalytics.core.utils.Util.getApplicationContext
import com.snowplowanalytics.core.utils.Util.getGeoLocationContext
+import com.snowplowanalytics.core.utils.Util.truncateUrlScheme
import com.snowplowanalytics.snowplow.configuration.PlatformContextProperty
import com.snowplowanalytics.snowplow.entity.DeepLink
import com.snowplowanalytics.snowplow.event.*
@@ -583,10 +584,10 @@ class Tracker(
}
}
if (url != null) {
- payload.add(Parameters.PAGE_URL, url)
+ payload.add(Parameters.PAGE_URL, truncateUrlScheme(url))
}
if (referrer != null) {
- payload.add(Parameters.PAGE_REFR, referrer)
+ payload.add(Parameters.PAGE_REFR, truncateUrlScheme(referrer))
}
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerConfigurationUpdate.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerConfigurationUpdate.kt
deleted file mode 100644
index d780a8be7..000000000
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerConfigurationUpdate.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
- *
- * This program is licensed to you under the Apache License Version 2.0,
- * and you may not use this file except in compliance with the Apache License Version 2.0.
- * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the Apache License Version 2.0 is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
- */
-package com.snowplowanalytics.core.tracker
-
-import com.snowplowanalytics.snowplow.configuration.PlatformContextProperty
-import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration
-import com.snowplowanalytics.snowplow.tracker.DevicePlatform
-import com.snowplowanalytics.snowplow.tracker.LogLevel
-import com.snowplowanalytics.snowplow.tracker.LoggerDelegate
-import org.json.JSONObject
-
-class TrackerConfigurationUpdate : TrackerConfiguration {
- var sourceConfig: TrackerConfiguration? = null
- var isPaused = false
- private var appIdUpdated = false
- private var devicePlatformUpdated = false
- private var base64encodingUpdated = false
- private var logLevelUpdated = false
- private var loggerDelegateUpdated = false
- private var applicationContextUpdated = false
- private var platformContextUpdated = false
- private var geoLocationContextUpdated = false
- private var sessionContextUpdated = false
- private var deepLinkContextUpdated = false
- private var screenContextUpdated = false
- private var screenViewAutotrackingUpdated = false
- private var lifecycleAutotrackingUpdated = false
- private var installAutotrackingUpdated = false
- private var exceptionAutotrackingUpdated = false
- private var diagnosticAutotrackingUpdated = false
- private var userAnonymisationUpdated = false
- private var trackerVersionSuffixUpdated = false
- private var platformContextPropertiesUpdated = false
-
- constructor(appId: String) : super(appId)
- constructor(appId: String, jsonObject: JSONObject) : super(appId, jsonObject)
-
- override var appId: String
- get() = if (sourceConfig == null || appIdUpdated) super.appId else sourceConfig!!.appId
- set(value) {
- super.appId = value
- appIdUpdated = true
- }
-
- override var devicePlatform: DevicePlatform
- get() = if (sourceConfig == null || devicePlatformUpdated) super.devicePlatform else sourceConfig!!.devicePlatform
- set(value) {
- super.devicePlatform = value
- devicePlatformUpdated = true
- }
-
- override var base64encoding: Boolean
- get() = if (sourceConfig == null || base64encodingUpdated) super.base64encoding else sourceConfig!!.base64encoding
- set(value) {
- super.base64encoding = value
- base64encodingUpdated = true
- }
-
- override var logLevel: LogLevel
- get() = if (sourceConfig == null || logLevelUpdated) super.logLevel else sourceConfig!!.logLevel
- set(value) {
- super.logLevel = value
- logLevelUpdated = true
- }
-
- override var loggerDelegate: LoggerDelegate?
- get() = if (sourceConfig == null || loggerDelegateUpdated) super.loggerDelegate else sourceConfig!!.loggerDelegate
- set(value) {
- super.loggerDelegate = value
- loggerDelegateUpdated = true
- }
-
- override var sessionContext: Boolean
- get() = if (sourceConfig == null || sessionContextUpdated) super.sessionContext else sourceConfig!!.sessionContext
- set(value) {
- super.sessionContext = value
- sessionContextUpdated = true
- }
-
- override var applicationContext: Boolean
- get() = if (sourceConfig == null || applicationContextUpdated) super.applicationContext else sourceConfig!!.applicationContext
- set(value) {
- super.applicationContext = value
- applicationContextUpdated = true
- }
-
- override var platformContext: Boolean
- get() = if (sourceConfig == null || platformContextUpdated) super.platformContext else sourceConfig!!.platformContext
- set(value) {
- super.platformContext = value
- platformContextUpdated = true
- }
-
- override var geoLocationContext: Boolean
- get() = if (sourceConfig == null || geoLocationContextUpdated) super.geoLocationContext else sourceConfig!!.geoLocationContext
- set(value) {
- super.geoLocationContext = value
- geoLocationContextUpdated = true
- }
-
- override var deepLinkContext: Boolean
- get() = if (sourceConfig == null || deepLinkContextUpdated) super.deepLinkContext else sourceConfig!!.deepLinkContext
- set(value) {
- super.deepLinkContext = value
- deepLinkContextUpdated = true
- }
-
- override var screenContext: Boolean
- get() = if (sourceConfig == null || screenContextUpdated) super.screenContext else sourceConfig!!.screenContext
- set(value) {
- super.screenContext = value
- screenContextUpdated = true
- }
-
- override var screenViewAutotracking: Boolean
- get() = if (sourceConfig == null || screenViewAutotrackingUpdated) super.screenViewAutotracking else sourceConfig!!.screenViewAutotracking
- set(value) {
- super.screenViewAutotracking = value
- screenViewAutotrackingUpdated = true
- }
-
- override var lifecycleAutotracking: Boolean
- get() = if (sourceConfig == null || lifecycleAutotrackingUpdated) super.lifecycleAutotracking else sourceConfig!!.lifecycleAutotracking
- set(value) {
- super.lifecycleAutotracking = value
- lifecycleAutotrackingUpdated = true
- }
-
- override var installAutotracking: Boolean
- get() = if (sourceConfig == null || installAutotrackingUpdated) super.installAutotracking else sourceConfig!!.installAutotracking
- set(value) {
- super.installAutotracking = value
- installAutotrackingUpdated = true
- }
-
- override var exceptionAutotracking: Boolean
- get() = if (sourceConfig == null || exceptionAutotrackingUpdated) super.exceptionAutotracking else sourceConfig!!.exceptionAutotracking
- set(value) {
- super.exceptionAutotracking = value
- exceptionAutotrackingUpdated = true
- }
-
- override var diagnosticAutotracking: Boolean
- get() = if (sourceConfig == null || diagnosticAutotrackingUpdated) super.diagnosticAutotracking else sourceConfig!!.diagnosticAutotracking
- set(value) {
- super.diagnosticAutotracking = value
- diagnosticAutotrackingUpdated = true
- }
-
- override var userAnonymisation: Boolean
- get() = if (sourceConfig == null || userAnonymisationUpdated) super.userAnonymisation else sourceConfig!!.userAnonymisation
- set(value) {
- super.userAnonymisation = value
- userAnonymisationUpdated = true
- }
-
- override var trackerVersionSuffix: String?
- get() = if (sourceConfig == null || trackerVersionSuffixUpdated) super.trackerVersionSuffix else sourceConfig!!.trackerVersionSuffix
- set(value) {
- super.trackerVersionSuffix = value
- trackerVersionSuffixUpdated = true
- }
-
- override var platformContextProperties: List?
- get() = if (sourceConfig == null || platformContextPropertiesUpdated) super.platformContextProperties else sourceConfig!!.platformContextProperties
- set(value) {
- super.platformContextProperties = value
- platformContextPropertiesUpdated = true
- }
-}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerControllerImpl.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerControllerImpl.kt
index 30d4ecb6c..bccb86fa5 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerControllerImpl.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/tracker/TrackerControllerImpl.kt
@@ -15,8 +15,10 @@ package com.snowplowanalytics.core.tracker
import androidx.annotation.RestrictTo
import com.snowplowanalytics.core.Controller
import com.snowplowanalytics.core.session.SessionControllerImpl
+import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration
import com.snowplowanalytics.snowplow.controller.*
import com.snowplowanalytics.snowplow.event.Event
+import com.snowplowanalytics.snowplow.media.controller.MediaController
import com.snowplowanalytics.snowplow.tracker.BuildConfig
import com.snowplowanalytics.snowplow.tracker.DevicePlatform
import com.snowplowanalytics.snowplow.tracker.LogLevel
@@ -46,6 +48,8 @@ class TrackerControllerImpl // Constructors
}
override val plugins: PluginsController
get() = serviceProvider.pluginsController
+ override val media: MediaController
+ get() = serviceProvider.mediaController
// Control methods
override fun pause() {
@@ -208,8 +212,8 @@ class TrackerControllerImpl // Constructors
}
return serviceProvider.getOrMakeTracker()
}
- private val dirtyConfig: TrackerConfigurationUpdate
- get() = serviceProvider.trackerConfigurationUpdate
+ private val dirtyConfig: TrackerConfiguration
+ get() = serviceProvider.trackerConfiguration
companion object {
private val TAG = TrackerControllerImpl::class.java.simpleName
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/utils/Util.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/utils/Util.kt
index f2b06c67a..e812b038d 100755
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/utils/Util.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/utils/Util.kt
@@ -46,9 +46,14 @@ object Util {
@JvmStatic
fun getDateTimeFromTimestamp(timestamp: Long): String {
+ val date = Date(timestamp)
+ return getDateTimeFromDate(date)
+ }
+
+ @JvmStatic
+ fun getDateTimeFromDate(date: Date): String {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale("en"))
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
- val date = Date(timestamp)
return dateFormat.format(date)
}
@@ -363,4 +368,16 @@ object Util {
e.printStackTrace(pw)
return sw.toString()
}
+
+ /**
+ * Truncates the scheme of a URL to 16 characters to satisfy the validation for the page_url and page_refr properties.
+ */
+ fun truncateUrlScheme(url: String): String {
+ val parts = url.split("://")
+ if (parts.size > 1) {
+ val updatedParts = listOf(parts.first().take(16)) + parts.drop(1)
+ return updatedParts.joinToString("://")
+ }
+ return url
+ }
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/Snowplow.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/Snowplow.kt
index 49db2aec2..62140f7bd 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/Snowplow.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/Snowplow.kt
@@ -18,9 +18,9 @@ import androidx.core.util.Consumer
import androidx.core.util.Pair
import com.snowplowanalytics.snowplow.configuration.ConfigurationBundle
-import com.snowplowanalytics.core.remoteconfiguration.ConfigurationProvider
+import com.snowplowanalytics.core.remoteconfiguration.RemoteConfigurationProvider
import com.snowplowanalytics.snowplow.configuration.ConfigurationState
-import com.snowplowanalytics.core.remoteconfiguration.FetchedConfigurationBundle
+import com.snowplowanalytics.core.remoteconfiguration.RemoteConfigurationBundle
import com.snowplowanalytics.core.tracker.ServiceProvider
import com.snowplowanalytics.core.tracker.TrackerWebViewInterface
@@ -40,7 +40,7 @@ object Snowplow {
// Private properties
private var defaultServiceProvider: ServiceProvider? = null
private val serviceProviderInstances: MutableMap = HashMap()
- private var configurationProvider: ConfigurationProvider? = null
+ private var configurationProvider: RemoteConfigurationProvider? = null
/**
* The default tracker instance is the first created in the app, but that can be overridden programmatically
@@ -101,11 +101,11 @@ object Snowplow {
defaultBundleVersion: Int,
onSuccess: Consumer, ConfigurationState?>?>
) {
- configurationProvider = ConfigurationProvider(remoteConfiguration, defaultBundles, defaultBundleVersion)
+ configurationProvider = RemoteConfigurationProvider(remoteConfiguration, defaultBundles, defaultBundleVersion)
configurationProvider?.retrieveConfiguration(
context,
false
- ) { fetchedConfigurationPair: Pair ->
+ ) { fetchedConfigurationPair: Pair ->
val fetchedConfigurationBundle = fetchedConfigurationPair.first
val configurationState = fetchedConfigurationPair.second
val bundles = fetchedConfigurationBundle.configurationBundle
@@ -182,7 +182,7 @@ object Snowplow {
configurationProvider?.let { it.retrieveConfiguration(
context,
true
- ) { fetchedConfigurationPair: Pair ->
+ ) { fetchedConfigurationPair: Pair ->
val fetchedConfigurationBundle = fetchedConfigurationPair.first
val configurationState = fetchedConfigurationPair.second
val bundles = fetchedConfigurationBundle.configurationBundle
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/ConfigurationBundle.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/ConfigurationBundle.kt
index b374ad553..af23d369c 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/ConfigurationBundle.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/ConfigurationBundle.kt
@@ -26,6 +26,7 @@ class ConfigurationBundle @JvmOverloads constructor(
var trackerConfiguration: TrackerConfiguration? = null
var subjectConfiguration: SubjectConfiguration? = null
var sessionConfiguration: SessionConfiguration? = null
+ var emitterConfiguration: EmitterConfiguration? = null
val configurations: List
get() {
@@ -34,6 +35,7 @@ class ConfigurationBundle @JvmOverloads constructor(
trackerConfiguration?.let { array.add(it) }
subjectConfiguration?.let { array.add(it) }
sessionConfiguration?.let { array.add(it) }
+ emitterConfiguration?.let { array.add(it) }
return array
}
@@ -56,6 +58,32 @@ class ConfigurationBundle @JvmOverloads constructor(
json = jsonObject.optJSONObject("sessionConfiguration")
json?.let { sessionConfiguration = SessionConfiguration(it) }
+
+ json = jsonObject.optJSONObject("emitterConfiguration")
+ json?.let { emitterConfiguration = EmitterConfiguration(it) }
+ }
+
+ fun updateSourceConfig(sourceBundle: ConfigurationBundle) {
+ sourceBundle.networkConfiguration?.let {
+ if (networkConfiguration == null) networkConfiguration = NetworkConfiguration()
+ networkConfiguration?.sourceConfig = it
+ }
+ sourceBundle.trackerConfiguration?.let {
+ if (trackerConfiguration == null) trackerConfiguration = TrackerConfiguration()
+ trackerConfiguration?.sourceConfig = it
+ }
+ sourceBundle.subjectConfiguration?.let {
+ if (subjectConfiguration == null) subjectConfiguration = SubjectConfiguration()
+ subjectConfiguration?.sourceConfig = it
+ }
+ sourceBundle.sessionConfiguration?.let {
+ if (sessionConfiguration == null) sessionConfiguration = SessionConfiguration()
+ sessionConfiguration?.sourceConfig = it
+ }
+ sourceBundle.emitterConfiguration?.let {
+ if (emitterConfiguration == null) emitterConfiguration = EmitterConfiguration()
+ emitterConfiguration?.sourceConfig = it
+ }
}
// Copyable
@@ -65,6 +93,7 @@ class ConfigurationBundle @JvmOverloads constructor(
copy.trackerConfiguration = trackerConfiguration
copy.subjectConfiguration = subjectConfiguration
copy.sessionConfiguration = sessionConfiguration
+ copy.emitterConfiguration = emitterConfiguration
return copy
}
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/EmitterConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/EmitterConfiguration.kt
index 0f6bcb510..7178ddf5a 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/EmitterConfiguration.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/EmitterConfiguration.kt
@@ -17,6 +17,7 @@ import com.snowplowanalytics.core.emitter.EmitterDefaults
import com.snowplowanalytics.snowplow.emitter.BufferOption
import com.snowplowanalytics.snowplow.emitter.EventStore
import com.snowplowanalytics.snowplow.network.RequestCallback
+import org.json.JSONObject
/**
* Configure how the tracker should send the events to the collector.
@@ -29,17 +30,62 @@ import com.snowplowanalytics.snowplow.network.RequestCallback
* - byteLimitGet: 40000 bytes
* - byteLimitPost: 40000 bytes
*/
-open class EmitterConfiguration : Configuration, EmitterConfigurationInterface {
-
- override var bufferOption: BufferOption = EmitterDefaults.bufferOption
- override var emitRange: Int = EmitterDefaults.emitRange
- override var threadPoolSize: Int = EmitterDefaults.threadPoolSize
- override var byteLimitGet: Long = EmitterDefaults.byteLimitGet
- override var byteLimitPost: Long = EmitterDefaults.byteLimitPost
- override var requestCallback: RequestCallback? = null
- override var eventStore: EventStore? = null
- override var customRetryForStatusCodes: Map? = null
- override var serverAnonymisation: Boolean = EmitterDefaults.serverAnonymisation
+open class EmitterConfiguration() : Configuration, EmitterConfigurationInterface {
+
+ private var _isPaused: Boolean? = null
+ internal var isPaused: Boolean
+ get() = _isPaused ?: sourceConfig?.isPaused ?: false
+ set(value) { _isPaused = value }
+
+ /**
+ * Fallback configuration to read from in case requested values are not present in this configuration.
+ */
+ internal var sourceConfig: EmitterConfiguration? = null
+
+ private var _bufferOption: BufferOption? = null
+ override var bufferOption: BufferOption
+ get() = _bufferOption ?: sourceConfig?.bufferOption ?: BufferOption.DefaultGroup
+ set(value) { _bufferOption = value }
+
+ private var _emitRange: Int? = null
+ override var emitRange: Int
+ get() = _emitRange ?: sourceConfig?.emitRange ?: EmitterDefaults.emitRange
+ set(value) { _emitRange = value }
+
+ private var _threadPoolSize: Int? = null
+ override var threadPoolSize: Int
+ get() = _threadPoolSize ?: sourceConfig?.threadPoolSize ?: EmitterDefaults.threadPoolSize
+ set(value) { _threadPoolSize = value }
+
+ private var _byteLimitGet: Long? = null
+ override var byteLimitGet: Long
+ get() = _byteLimitGet ?: sourceConfig?.byteLimitGet ?: EmitterDefaults.byteLimitGet
+ set(value) { _byteLimitGet = value }
+
+ private var _byteLimitPost: Long? = null
+ override var byteLimitPost: Long
+ get() = _byteLimitPost ?: sourceConfig?.byteLimitPost ?: EmitterDefaults.byteLimitPost
+ set(value) { _byteLimitPost = value }
+
+ private var _requestCallback: RequestCallback? = null
+ override var requestCallback: RequestCallback?
+ get() = _requestCallback ?: sourceConfig?.requestCallback
+ set(value) { _requestCallback = value }
+
+ private var _eventStore: EventStore? = null
+ override var eventStore: EventStore?
+ get() = _eventStore ?: sourceConfig?.eventStore
+ set(value) { _eventStore = value }
+
+ private var _customRetryForStatusCodes: Map? = null
+ override var customRetryForStatusCodes: Map?
+ get() = _customRetryForStatusCodes ?: sourceConfig?.customRetryForStatusCodes
+ set(value) { _customRetryForStatusCodes = value }
+
+ private var _serverAnonymisation: Boolean? = null
+ override var serverAnonymisation: Boolean
+ get() = _serverAnonymisation ?: sourceConfig?.serverAnonymisation ?: EmitterDefaults.serverAnonymisation
+ set(value) { _serverAnonymisation = value }
// Builders
@@ -135,4 +181,29 @@ open class EmitterConfiguration : Configuration, EmitterConfigurationInterface {
.customRetryForStatusCodes(customRetryForStatusCodes)
.serverAnonymisation(serverAnonymisation)
}
+
+ // JSON Formatter
+ /**
+ * This constructor is used in remote configuration.
+ */
+ constructor(jsonObject: JSONObject) : this() {
+ if (jsonObject.has("bufferOption")) {
+ _bufferOption = BufferOption.valueOf(jsonObject.getString("bufferOption"))
+ }
+ if (jsonObject.has("emitRange")) { _emitRange = jsonObject.getInt("emitRange") }
+ if (jsonObject.has("threadPoolSize")) { _threadPoolSize = jsonObject.getInt("threadPoolSize") }
+ if (jsonObject.has("byteLimitGet")) { _byteLimitGet = jsonObject.getLong("byteLimitGet") }
+ if (jsonObject.has("byteLimitPost")) { _byteLimitPost = jsonObject.getLong("byteLimitPost") }
+ if (jsonObject.has("serverAnonymisation")) { _serverAnonymisation = jsonObject.getBoolean("serverAnonymisation") }
+ if (jsonObject.has("customRetryForStatusCodes")) {
+ val customRetryForStatusCodes = mutableMapOf()
+ val customRetryForStatusCodesJson = jsonObject.getJSONObject("customRetryForStatusCodes")
+ val keys = customRetryForStatusCodesJson.keys()
+ while (keys.hasNext()) {
+ val key = keys.next()
+ customRetryForStatusCodes[key.toInt()] = customRetryForStatusCodesJson.getBoolean(key)
+ }
+ _customRetryForStatusCodes = customRetryForStatusCodes
+ }
+ }
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/GdprConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/GdprConfiguration.kt
index 30f90c736..c64320cc7 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/GdprConfiguration.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/GdprConfiguration.kt
@@ -12,6 +12,7 @@
*/
package com.snowplowanalytics.snowplow.configuration
+import com.snowplowanalytics.core.gdpr.Gdpr
import com.snowplowanalytics.core.gdpr.GdprConfigurationInterface
import com.snowplowanalytics.snowplow.util.Basis
@@ -19,35 +20,76 @@ import com.snowplowanalytics.snowplow.util.Basis
* Allows the GDPR configuration of the tracker. Provide a [GdprConfiguration] when creating a tracker
* to attach a GDPR entity to every event.
*/
-open class GdprConfiguration
-/**
- * Enables GDPR entity to be sent with each event.
- *
- * @param basisForProcessing GDPR Basis for processing.
- * @param documentId ID of a GDPR basis document.
- * @param documentVersion Version of the document.
- * @param documentDescription Description of the document.
- */(
+open class GdprConfiguration : Configuration, GdprConfigurationInterface {
+
+ /**
+ * Fallback configuration to read from in case requested values are not present in this configuration.
+ */
+ internal var sourceConfig: GdprConfiguration? = null
+
+ private var _isEnabled: Boolean? = null
+ internal var isEnabled: Boolean
+ get() = _isEnabled ?: sourceConfig?.isEnabled ?: true
+ set(value) { _isEnabled = value }
+
+ private var _gdpr: Gdpr? = null
+ internal var gdpr: Gdpr?
+ get() = _gdpr ?: sourceConfig?.gdpr
+ set(value) { _gdpr = value }
+
+ private var _basisForProcessing: Basis? = null
/**
* Basis for processing.
*/
- override val basisForProcessing: Basis,
-
+ override var basisForProcessing: Basis
+ get() = _basisForProcessing ?: sourceConfig?.basisForProcessing ?: Basis.CONTRACT
+ set(value) { _basisForProcessing = value }
+
+ private var _documentId: String? = null
/**
* ID of a GDPR basis document.
*/
- override val documentId: String?,
-
+ override var documentId: String?
+ get() = _documentId ?: sourceConfig?.documentId
+ set(value) { _documentId = value }
+
+ private var _documentVersion: String? = null
/**
* Version of the document.
*/
- override val documentVersion: String?,
-
+ override var documentVersion: String?
+ get() = _documentVersion ?: sourceConfig?.documentVersion
+ set(value) { _documentVersion = value }
+
+ private var _documentDescription: String? = null
/**
* Description of the document.
*/
- override val documentDescription: String?
-) : Configuration, GdprConfigurationInterface {
+ override var documentDescription: String?
+ get() = _documentDescription ?: sourceConfig?.documentDescription
+ set(value) { _documentDescription = value }
+
+ /**
+ * Enables GDPR entity to be sent with each event.
+ *
+ * @param basisForProcessing GDPR Basis for processing.
+ * @param documentId ID of a GDPR basis document.
+ * @param documentVersion Version of the document.
+ * @param documentDescription Description of the document.
+ */
+ constructor(
+ basisForProcessing: Basis,
+ documentId: String?,
+ documentVersion: String?,
+ documentDescription: String?
+ ) {
+ this._basisForProcessing = basisForProcessing
+ this._documentId = documentId
+ this._documentVersion = documentVersion
+ this._documentDescription = documentDescription
+ }
+
+ internal constructor()
// Copyable
override fun copy(): GdprConfiguration {
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/NetworkConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/NetworkConfiguration.kt
index 75ebedff7..4541d8fa6 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/NetworkConfiguration.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/NetworkConfiguration.kt
@@ -37,25 +37,65 @@ import java.util.*
* timeout: 5 seconds
*/
class NetworkConfiguration : NetworkConfigurationInterface, Configuration {
+
+ /**
+ * Fallback configuration to read from in case requested values are not present in this configuration.
+ */
+ internal var sourceConfig: NetworkConfiguration? = null
+
+ private var _endpoint: String? = null
/**
* @return URL (without schema/protocol) used to send events to the collector.
*/
- override var endpoint: String? = null
+ override var endpoint: String?
+ get() = _endpoint ?: sourceConfig?.endpoint
+ set(value) { _endpoint = value }
+ private var _method: HttpMethod? = null
/**
* @return Method (GET or POST) used to send events to the collector.
*/
- override var method: HttpMethod = EmitterDefaults.httpMethod
+ override var method: HttpMethod
+ get() = _method ?: sourceConfig?.method ?: EmitterDefaults.httpMethod
+ set(value) { _method = value }
+ private var _protocol: Protocol? = null
/**
* @return Protocol (HTTP or HTTPS) used to send events to the collector.
*/
- override var protocol: Protocol? = EmitterDefaults.requestSecurity
- override var networkConnection: NetworkConnection? = null
- override var customPostPath: String? = null
- override var timeout: Int? = EmitterDefaults.emitTimeout
- override var okHttpClient: OkHttpClient? = null
- override var okHttpCookieJar: CookieJar? = null
+ override var protocol: Protocol?
+ get() = _protocol ?: sourceConfig?.protocol ?: EmitterDefaults.httpProtocol
+ set(value) { _protocol = value }
+
+ private var _networkConnection: NetworkConnection? = null
+ override var networkConnection: NetworkConnection?
+ get() = _networkConnection ?: sourceConfig?.networkConnection
+ set(value) { _networkConnection = value }
+
+ private var _customPostPath: String? = null
+ override var customPostPath: String?
+ get() = _customPostPath ?: sourceConfig?.customPostPath
+ set(value) { _customPostPath = value }
+
+ private var _timeout: Int? = null
+ override var timeout: Int?
+ get() = _timeout ?: sourceConfig?.timeout ?: EmitterDefaults.emitTimeout
+ set(value) { _timeout = value }
+
+ private var _okHttpClient: OkHttpClient? = null
+ override var okHttpClient: OkHttpClient?
+ get() = _okHttpClient ?: sourceConfig?.okHttpClient
+ set(value) { _okHttpClient = value }
+
+ private var _okHttpCookieJar: CookieJar? = null
+ override var okHttpCookieJar: CookieJar?
+ get() = _okHttpCookieJar ?: sourceConfig?.okHttpCookieJar
+ set(value) { _okHttpCookieJar = value }
+
+ private var _requestHeaders: Map? = null
+ override var requestHeaders: Map?
+ get() = _requestHeaders ?: sourceConfig?.requestHeaders
+ set(value) { _requestHeaders = value }
// Constructors
@@ -100,6 +140,7 @@ class NetworkConfiguration : NetworkConfigurationInterface, Configuration {
this.networkConnection = networkConnection
}
+ internal constructor() {}
// Builder methods
@@ -145,6 +186,14 @@ class NetworkConfiguration : NetworkConfigurationInterface, Configuration {
return this
}
+ /**
+ * Custom headers to add to HTTP requests to the collector.
+ */
+ fun requestHeaders(requestHeaders: Map): NetworkConfiguration {
+ this.requestHeaders = requestHeaders
+ return this
+ }
+
// Copyable
override fun copy(): Configuration {
val copy: NetworkConfiguration = if (networkConnection != null) {
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SessionConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SessionConfiguration.kt
index 59204d974..c12294089 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SessionConfiguration.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SessionConfiguration.kt
@@ -13,7 +13,9 @@
package com.snowplowanalytics.snowplow.configuration
import androidx.core.util.Consumer
+import com.snowplowanalytics.core.session.Session
import com.snowplowanalytics.core.session.SessionConfigurationInterface
+import com.snowplowanalytics.core.tracker.TrackerDefaults
import com.snowplowanalytics.snowplow.tracker.SessionState
import com.snowplowanalytics.snowplow.util.TimeMeasure
import org.json.JSONObject
@@ -32,32 +34,46 @@ import java.util.concurrent.TimeUnit
*
* @see [TrackerConfiguration.sessionContext]
*/
-open class SessionConfiguration
-/**
- * This will set up the session behaviour of the tracker.
- * @param foregroundTimeout The timeout set for the inactivity of app when in foreground.
- * @param backgroundTimeout The timeout set for the inactivity of app when in background.
- */(
- /**
- * The amount of time that can elapse before the
- * session id is updated while the app is in the
- * foreground.
- */
- override var foregroundTimeout: TimeMeasure,
-
+open class SessionConfiguration : SessionConfigurationInterface, Configuration {
+
+ private var _isPaused: Boolean? = null
+ internal var isPaused: Boolean
+ get() = _isPaused ?: sourceConfig?.isPaused ?: false
+ set(value) { _isPaused = value }
+
/**
- * The amount of time that can elapse before the
- * session id is updated while the app is in the
- * background.
+ * Fallback configuration to read from in case requested values are not present in this configuration.
*/
+ internal var sourceConfig: SessionConfiguration? = null
+
+ private var _foregroundTimeout: TimeMeasure? = null
+ override var foregroundTimeout: TimeMeasure
+ get() = _foregroundTimeout ?: sourceConfig?.foregroundTimeout ?: TimeMeasure(TrackerDefaults.foregroundTimeout, TimeUnit.SECONDS)
+ set(value) { _foregroundTimeout = value }
+
+ private var _backgroundTimeout: TimeMeasure? = null
override var backgroundTimeout: TimeMeasure
-) : SessionConfigurationInterface, Configuration {
-
+ get() = _backgroundTimeout ?: sourceConfig?.backgroundTimeout ?: TimeMeasure(TrackerDefaults.backgroundTimeout, TimeUnit.SECONDS)
+ set(value) { _backgroundTimeout = value }
+
+ private var _onSessionUpdate: Consumer? = null
/**
* The callback called every time the session is updated.
*/
- override var onSessionUpdate: Consumer? = null
-
+ override var onSessionUpdate: Consumer?
+ get() = _onSessionUpdate ?: sourceConfig?.onSessionUpdate
+ set(value) { _onSessionUpdate = value }
+
+ /**
+ * This will set up the session behaviour of the tracker.
+ * @param foregroundTimeout The timeout set for the inactivity of app when in foreground.
+ * @param backgroundTimeout The timeout set for the inactivity of app when in background.
+ */
+ constructor(foregroundTimeout: TimeMeasure? = null, backgroundTimeout: TimeMeasure? = null) {
+ foregroundTimeout?.let { this._foregroundTimeout = it }
+ backgroundTimeout?.let { this._backgroundTimeout = it }
+ }
+
// Builders
fun onSessionUpdate(onSessionUpdate: Consumer?): SessionConfiguration {
@@ -79,9 +95,13 @@ open class SessionConfiguration
TimeMeasure(30, TimeUnit.MINUTES),
TimeMeasure(30, TimeUnit.MINUTES)
) {
- val foregroundTimeout = jsonObject.optInt("foregroundTimeout", 1800)
- val backgroundTimeout = jsonObject.optInt("backgroundTimeout", 1800)
- this.foregroundTimeout = TimeMeasure(foregroundTimeout.toLong(), TimeUnit.SECONDS)
- this.backgroundTimeout = TimeMeasure(backgroundTimeout.toLong(), TimeUnit.SECONDS)
+ if (jsonObject.has("foregroundTimeout")) {
+ val foregroundTimeout = jsonObject.getInt("foregroundTimeout")
+ this._foregroundTimeout = TimeMeasure(foregroundTimeout.toLong(), TimeUnit.SECONDS)
+ }
+ if (jsonObject.has("backgroundTimeout")) {
+ val backgroundTimeout = jsonObject.getInt("backgroundTimeout")
+ this._backgroundTimeout = TimeMeasure(backgroundTimeout.toLong(), TimeUnit.SECONDS)
+ }
}
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SubjectConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SubjectConfiguration.kt
index d87ac50db..3e15d81c3 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SubjectConfiguration.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/SubjectConfiguration.kt
@@ -22,16 +22,60 @@ import org.json.JSONObject
*/
open class SubjectConfiguration() : Configuration, SubjectConfigurationInterface {
- override var userId: String? = null
- override var networkUserId: String? = null
- override var domainUserId: String? = null
- override var useragent: String? = null
- override var ipAddress: String? = null
- override var timezone: String? = null
- override var language: String? = null
- override var screenResolution: Size? = null
- override var screenViewPort: Size? = null
- override var colorDepth: Int? = null
+ /**
+ * Fallback configuration to read from in case requested values are not present in this configuration.
+ */
+ internal var sourceConfig: SubjectConfiguration? = null
+
+ private var _userId: String? = null
+ override var userId: String?
+ get() = _userId ?: sourceConfig?.userId
+ set(value) { _userId = value }
+
+ private var _networkUserId: String? = null
+ override var networkUserId: String?
+ get() = _networkUserId ?: sourceConfig?.networkUserId
+ set(value) { _networkUserId = value }
+
+ private var _domainUserId: String? = null
+ override var domainUserId: String?
+ get() = _domainUserId ?: sourceConfig?.domainUserId
+ set(value) { _domainUserId = value }
+
+ private var _useragent: String? = null
+ override var useragent: String?
+ get() = _useragent ?: sourceConfig?.useragent
+ set(value) { _useragent = value }
+
+ private var _ipAddress: String? = null
+ override var ipAddress: String?
+ get() = _ipAddress ?: sourceConfig?.ipAddress
+ set(value) { _ipAddress = value }
+
+ private var _timezone: String? = null
+ override var timezone: String?
+ get() = _timezone ?: sourceConfig?.timezone
+ set(value) { _timezone = value }
+
+ private var _language: String? = null
+ override var language: String?
+ get() = _language ?: sourceConfig?.language
+ set(value) { _language = value }
+
+ private var _screenResolution: Size? = null
+ override var screenResolution: Size?
+ get() = _screenResolution ?: sourceConfig?.screenResolution
+ set(value) { _screenResolution = value }
+
+ private var _screenViewPort: Size? = null
+ override var screenViewPort: Size?
+ get() = _screenViewPort ?: sourceConfig?.screenViewPort
+ set(value) { _screenViewPort = value }
+
+ private var _colorDepth: Int? = null
+ override var colorDepth: Int?
+ get() = _colorDepth ?: sourceConfig?.colorDepth
+ set(value) { _colorDepth = value }
// Builder methods
@@ -144,14 +188,12 @@ open class SubjectConfiguration() : Configuration, SubjectConfigurationInterface
* This constructor is used in remote configuration.
*/
constructor(jsonObject: JSONObject) : this() {
- userId = if (jsonObject.has("userId")) jsonObject.optString("userId") else null
- networkUserId =
- if (jsonObject.has("networkUserId")) jsonObject.optString("networkUserId") else null
- domainUserId =
- if (jsonObject.has("domainUserId")) jsonObject.optString("domainUserId") else null
- useragent = if (jsonObject.has("useragent")) jsonObject.optString("useragent") else null
- ipAddress = if (jsonObject.has("ipAddress")) jsonObject.optString("ipAddress") else null
- timezone = if (jsonObject.has("timezone")) jsonObject.optString("timezone") else null
- language = if (jsonObject.has("language")) jsonObject.optString("language") else null
+ if (jsonObject.has("userId")) { _userId = jsonObject.optString("userId") }
+ if (jsonObject.has("networkUserId")) { _networkUserId = jsonObject.optString("networkUserId") }
+ if (jsonObject.has("domainUserId")) { _domainUserId = jsonObject.optString("domainUserId") }
+ if (jsonObject.has("useragent")) { _useragent = jsonObject.optString("useragent") }
+ if (jsonObject.has("ipAddress")) { _ipAddress = jsonObject.optString("ipAddress") }
+ if (jsonObject.has("timezone")) { _timezone = jsonObject.optString("timezone") }
+ if (jsonObject.has("language")) { _language = jsonObject.optString("language") }
}
}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/TrackerConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/TrackerConfiguration.kt
index 1fb385bbe..688ac83d1 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/TrackerConfiguration.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/configuration/TrackerConfiguration.kt
@@ -44,37 +44,121 @@ import java.util.*
* - exceptionAutotracking: true
* - diagnosticAutotracking: false
* - userAnonymisation: false
- *
- * @param appId Identifier of the app.
*/
-open class TrackerConfiguration(
+open class TrackerConfiguration : TrackerConfigurationInterface, Configuration {
+
+ /**
+ * Identifier of the app.
+ */
+ private var _appId: String? = null
override var appId: String
-) : TrackerConfigurationInterface, Configuration {
-
- override var devicePlatform: DevicePlatform = TrackerDefaults.devicePlatform
- override var base64encoding: Boolean = TrackerDefaults.base64Encoded
- override var logLevel: LogLevel = TrackerDefaults.logLevel
- override var loggerDelegate: LoggerDelegate? = null
- override var sessionContext: Boolean = TrackerDefaults.sessionContext
- override var applicationContext: Boolean = TrackerDefaults.applicationContext
- override var platformContext: Boolean = TrackerDefaults.platformContext
- override var geoLocationContext: Boolean = TrackerDefaults.geoLocationContext
- override var deepLinkContext: Boolean = TrackerDefaults.deepLinkContext
- override var screenContext: Boolean = TrackerDefaults.screenContext
- override var screenViewAutotracking: Boolean = TrackerDefaults.screenViewAutotracking
- override var lifecycleAutotracking: Boolean = TrackerDefaults.lifecycleAutotracking
- override var installAutotracking: Boolean = TrackerDefaults.installAutotracking
- override var exceptionAutotracking: Boolean = TrackerDefaults.exceptionAutotracking
- override var diagnosticAutotracking: Boolean = TrackerDefaults.diagnosticAutotracking
- override var userAnonymisation: Boolean = TrackerDefaults.userAnonymisation
- override var trackerVersionSuffix: String? = null
+ get() = _appId ?: sourceConfig?.appId ?: ""
+ set(value) { if (value.isNotEmpty()) { _appId = value } }
+
+ /**
+ * Fallback configuration to read from in case requested values are not present in this configuration.
+ */
+ var sourceConfig: TrackerConfiguration? = null
+
+ private var _isPaused: Boolean? = null
+ internal var isPaused: Boolean
+ get() = _isPaused ?: sourceConfig?.isPaused ?: false
+ set(value) { _isPaused = value }
+
+ private var _devicePlatform: DevicePlatform? = null
+ override var devicePlatform: DevicePlatform
+ get() = _devicePlatform ?: sourceConfig?.devicePlatform ?: TrackerDefaults.devicePlatform
+ set(value) { _devicePlatform = value }
+
+ private var _base64encoding: Boolean? = null
+ override var base64encoding: Boolean
+ get() = _base64encoding ?: sourceConfig?.base64encoding ?: TrackerDefaults.base64Encoded
+ set(value) { _base64encoding = value }
+
+ private var _logLevel: LogLevel? = null
+ override var logLevel: LogLevel
+ get() = _logLevel ?: sourceConfig?.logLevel ?: TrackerDefaults.logLevel
+ set(value) { _logLevel = value }
+
+ private var _loggerDelegate: LoggerDelegate? = null
+ override var loggerDelegate: LoggerDelegate?
+ get() = _loggerDelegate ?: sourceConfig?.loggerDelegate
+ set(value) { _loggerDelegate = value }
+
+ private var _sessionContext: Boolean? = null
+ override var sessionContext: Boolean
+ get() = _sessionContext ?: sourceConfig?.sessionContext ?: TrackerDefaults.sessionContext
+ set(value) { _sessionContext = value }
+
+ private var _applicationContext: Boolean? = null
+ override var applicationContext: Boolean
+ get() = _applicationContext ?: sourceConfig?.applicationContext ?: TrackerDefaults.applicationContext
+ set(value) { _applicationContext = value }
+
+ private var _platformContext: Boolean? = null
+ override var platformContext: Boolean
+ get() = _platformContext ?: sourceConfig?.platformContext ?: TrackerDefaults.platformContext
+ set(value) { _platformContext = value }
+
+ private var _geoLocationContext: Boolean? = null
+ override var geoLocationContext: Boolean
+ get() = _geoLocationContext ?: sourceConfig?.geoLocationContext ?: TrackerDefaults.geoLocationContext
+ set(value) { _geoLocationContext = value }
+
+ private var _deepLinkContext: Boolean? = null
+ override var deepLinkContext: Boolean
+ get() = _deepLinkContext ?: sourceConfig?.deepLinkContext ?: TrackerDefaults.deepLinkContext
+ set(value) { _deepLinkContext = value }
+ private var _screenContext: Boolean? = null
+ override var screenContext: Boolean
+ get() = _screenContext ?: sourceConfig?.screenContext ?: TrackerDefaults.screenContext
+ set(value) { _screenContext = value }
+
+ private var _screenViewAutotracking: Boolean? = null
+ override var screenViewAutotracking: Boolean
+ get() = _screenViewAutotracking ?: sourceConfig?.screenViewAutotracking ?: TrackerDefaults.screenViewAutotracking
+ set(value) { _screenViewAutotracking = value }
+
+ private var _lifecycleAutotracking: Boolean? = null
+ override var lifecycleAutotracking: Boolean
+ get() = _lifecycleAutotracking ?: sourceConfig?.lifecycleAutotracking ?: TrackerDefaults.lifecycleAutotracking
+ set(value) { _lifecycleAutotracking = value }
+
+ private var _installAutotracking: Boolean? = null
+ override var installAutotracking: Boolean
+ get() = _installAutotracking ?: sourceConfig?.installAutotracking ?: TrackerDefaults.installAutotracking
+ set(value) { _installAutotracking = value }
+
+ private var _exceptionAutotracking: Boolean? = null
+ override var exceptionAutotracking: Boolean
+ get() = _exceptionAutotracking ?: sourceConfig?.exceptionAutotracking ?: TrackerDefaults.exceptionAutotracking
+ set(value) { _exceptionAutotracking = value }
+
+ private var _diagnosticAutotracking: Boolean? = null
+ override var diagnosticAutotracking: Boolean
+ get() = _diagnosticAutotracking ?: sourceConfig?.diagnosticAutotracking ?: TrackerDefaults.diagnosticAutotracking
+ set(value) { _diagnosticAutotracking = value }
+
+ private var _userAnonymisation: Boolean? = null
+ override var userAnonymisation: Boolean
+ get() = _userAnonymisation ?: sourceConfig?.userAnonymisation ?: TrackerDefaults.userAnonymisation
+ set(value) { _userAnonymisation = value }
+
+ private var _trackerVersionSuffix: String? = null
+ override var trackerVersionSuffix: String?
+ get() = _trackerVersionSuffix ?: sourceConfig?.trackerVersionSuffix
+ set(value) { _trackerVersionSuffix = value }
+
+ private var _platformContextProperties: List? = null
/**
* List of properties of the platform context to track.
* If not passed and `platformContext` is enabled, all available properties will be tracked.
* The required `osType`, `osVersion`, `deviceManufacturer`, and `deviceModel` properties will be tracked in the entity regardless of this setting.
*/
- open var platformContextProperties: List? = null
+ open var platformContextProperties: List?
+ get() = _platformContextProperties ?: sourceConfig?.platformContextProperties
+ set(value) { _platformContextProperties = value }
// Builder methods
@@ -269,6 +353,18 @@ open class TrackerConfiguration(
.platformContextProperties(platformContextProperties)
}
+ /**
+ * @param appId Identifier of the app.
+ */
+ constructor(appId: String) {
+ this._appId = appId
+ }
+
+ /**
+ * This constructor is only used internally in the service provider
+ */
+ internal constructor()
+
// JSON Formatter
/**
* This constructor is used in remote configuration.
@@ -280,32 +376,30 @@ open class TrackerConfiguration(
)
) {
val value = jsonObject.optString("devicePlatform", DevicePlatform.Mobile.value)
- devicePlatform = DevicePlatform.getByValue(value)
- base64encoding = jsonObject.optBoolean("base64encoding", base64encoding)
-
- val log = jsonObject.optString("logLevel", LogLevel.OFF.name)
- try {
- logLevel = LogLevel.valueOf(log.uppercase(Locale.getDefault()))
- } catch (e: Exception) {
- Logger.e(TAG, "Unable to decode logLevel from remote configuration.")
+ _devicePlatform = DevicePlatform.getByValue(value)
+ if (jsonObject.has("base64encoding")) { _base64encoding = jsonObject.getBoolean("base64encoding") }
+
+ if (jsonObject.has("logLevel")) {
+ val log = jsonObject.optString("logLevel", LogLevel.OFF.name)
+ try {
+ _logLevel = LogLevel.valueOf(log.uppercase(Locale.getDefault()))
+ } catch (e: Exception) {
+ Logger.e(TAG, "Unable to decode logLevel from remote configuration.")
+ }
}
-
- sessionContext = jsonObject.optBoolean("sessionContext", sessionContext)
- applicationContext = jsonObject.optBoolean("applicationContext", applicationContext)
- platformContext = jsonObject.optBoolean("platformContext", platformContext)
- geoLocationContext = jsonObject.optBoolean("geoLocationContext", geoLocationContext)
- screenContext = jsonObject.optBoolean("screenContext", screenContext)
- deepLinkContext = jsonObject.optBoolean("deepLinkContext", deepLinkContext)
- screenViewAutotracking =
- jsonObject.optBoolean("screenViewAutotracking", screenViewAutotracking)
- lifecycleAutotracking =
- jsonObject.optBoolean("lifecycleAutotracking", lifecycleAutotracking)
- installAutotracking = jsonObject.optBoolean("installAutotracking", installAutotracking)
- exceptionAutotracking =
- jsonObject.optBoolean("exceptionAutotracking", exceptionAutotracking)
- diagnosticAutotracking =
- jsonObject.optBoolean("diagnosticAutotracking", diagnosticAutotracking)
- userAnonymisation = jsonObject.optBoolean("userAnonymisation", userAnonymisation)
+
+ if (jsonObject.has("sessionContext")) { _sessionContext = jsonObject.getBoolean("sessionContext") }
+ if (jsonObject.has("applicationContext")) { _applicationContext = jsonObject.getBoolean("applicationContext") }
+ if (jsonObject.has("platformContext")) { _platformContext = jsonObject.getBoolean("platformContext") }
+ if (jsonObject.has("geoLocationContext")) { _geoLocationContext = jsonObject.getBoolean("geoLocationContext") }
+ if (jsonObject.has("screenContext")) { _screenContext = jsonObject.getBoolean("screenContext") }
+ if (jsonObject.has("deepLinkContext")) { _deepLinkContext = jsonObject.getBoolean("deepLinkContext") }
+ if (jsonObject.has("screenViewAutotracking")) { _screenViewAutotracking = jsonObject.getBoolean("screenViewAutotracking") }
+ if (jsonObject.has("lifecycleAutotracking")) { _lifecycleAutotracking = jsonObject.getBoolean("lifecycleAutotracking") }
+ if (jsonObject.has("installAutotracking")) { _installAutotracking = jsonObject.getBoolean("installAutotracking") }
+ if (jsonObject.has("exceptionAutotracking")) { _exceptionAutotracking = jsonObject.getBoolean("exceptionAutotracking") }
+ if (jsonObject.has("diagnosticAutotracking")) { _diagnosticAutotracking = jsonObject.getBoolean("diagnosticAutotracking") }
+ if (jsonObject.has("userAnonymisation")) { _userAnonymisation = jsonObject.getBoolean("userAnonymisation") }
}
companion object {
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/controller/TrackerController.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/controller/TrackerController.kt
index 8f4bc02a9..e28fc2226 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/controller/TrackerController.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/controller/TrackerController.kt
@@ -14,6 +14,7 @@ package com.snowplowanalytics.snowplow.controller
import com.snowplowanalytics.core.tracker.TrackerConfigurationInterface
import com.snowplowanalytics.snowplow.event.Event
+import com.snowplowanalytics.snowplow.media.controller.MediaController
import java.util.*
/**
@@ -78,6 +79,11 @@ interface TrackerController : TrackerConfigurationInterface {
* Note: don't retain the reference. It may change on tracker reconfiguration.
*/
val plugins: PluginsController
+
+ /**
+ * Media controller for managing media tracking instances and tracking media events.
+ */
+ val media: MediaController
// Methods
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/configuration/MediaTrackingConfiguration.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/configuration/MediaTrackingConfiguration.kt
new file mode 100644
index 000000000..96c92f950
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/configuration/MediaTrackingConfiguration.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.configuration
+
+import com.snowplowanalytics.snowplow.event.Event
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
+import kotlin.reflect.KClass
+
+/**
+ * Configuration for a `MediaTracking` instance.
+ *
+ * @param id Unique identifier for the media tracking instance. The same ID is used for media player session if enabled.
+ * @param player Properties for the media player context entity attached to media events.
+ * @param pings Whether to track media ping events. Defaults to true.
+ * @param pingInterval Interval in seconds in which the media ping events are tracked. Defaults to 30 seconds unless `pings` are disabled.
+ * @param maxPausedPings Maximum number of consecutive ping events to send when playback is paused. Defaults to 1 unless `pings` are disabled.
+ * @param session Whether to track the media player session context entity along with media events. Defaults to true. The session entity contain the `id` identifier as well as statistics about the media playback.
+ * @param boundaries Percentage boundaries of the video to track percent progress events at.
+ * @param entities Additional context entities to attach to media events.
+ * @param captureEvents List of event types to allow tracking. If not specified (`null`), all tracked events will be allowed and tracked. Otherwise, tracked event types not present in the list will be discarded.
+ */
+data class MediaTrackingConfiguration @JvmOverloads constructor(
+ val id: String,
+ var player: MediaPlayerEntity? = null,
+ var pings: Boolean = true,
+ var pingInterval: Int? = null,
+ var maxPausedPings: Int? = null,
+ var session: Boolean = true,
+ var boundaries: List? = null,
+ var entities: List? = null,
+ var captureEvents: List>? = null,
+ ) {
+ fun setCaptureEvents(captureEvents: List>?): MediaTrackingConfiguration {
+ this.captureEvents = captureEvents?.map { it.kotlin }
+ return this
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/controller/MediaController.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/controller/MediaController.kt
new file mode 100644
index 000000000..ec43012dc
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/controller/MediaController.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.controller
+
+import com.snowplowanalytics.snowplow.media.configuration.MediaTrackingConfiguration
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Controller for managing media tracking instances and tracking media events.
+ */
+interface MediaController {
+ /**
+ * Starts media tracking for a single media content tracked in a media player.
+ *
+ * @param id Unique identifier for the media tracking instance. The same ID will be used for media player session if enabled.
+ * @param player Properties for the media player context entity attached to media events.
+ */
+ fun startMediaTracking(id: String, player: MediaPlayerEntity? = null): MediaTracking
+
+ /**
+ * Starts media tracking for a single media content tracked in a media player.
+ *
+ * @param configuration Configuration for the media tracking instance.
+ */
+ fun startMediaTracking(configuration: MediaTrackingConfiguration): MediaTracking
+
+ /**
+ * Returns a media tracking instance for the given ID.
+ *
+ * @param id Unique identifier for the media tracking instance.
+ */
+ fun getMediaTracking(id: String): MediaTracking?
+
+ /**
+ * Ends autotracked events and cleans the media tracking instance.
+ *
+ * @param id Unique identifier for the media tracking instance.
+ */
+ fun endMediaTracking(id: String)
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/controller/MediaTracking.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/controller/MediaTracking.kt
new file mode 100644
index 000000000..7b76ad06a
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/controller/MediaTracking.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.controller
+
+import com.snowplowanalytics.snowplow.event.Event
+import com.snowplowanalytics.snowplow.media.entity.MediaAdBreakEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaAdEntity
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Media tracking instance with methods to track media events.
+ */
+interface MediaTracking {
+ /**
+ * Unique identifier for the media tracking instance.
+ The same ID is used for media player session if enabled.
+ */
+ val id: String
+
+ /**
+ * Updates stored attributes of the media player such as the current playback.
+ * Use this function to continually update the player attributes so that they can be sent in the background ping events.
+ *
+ * @param player Updates to the properties for the media player context entity attached to media events.
+ * @param ad Updates to the properties for the ad context entity attached to media events during ad playback.
+ * @param adBreak Updates to the properties for the ad break context entity attached to media events during ad break playback.
+ */
+ fun update(
+ player: MediaPlayerEntity? = null,
+ ad: MediaAdEntity? = null,
+ adBreak: MediaAdBreakEntity? = null
+ )
+
+ /**
+ * Tracks a media player event along with the media entities (e.g., player, session, ad).
+ *
+ * @param event The media player event to track.
+ * @param player Updates to the properties for the media player context entity attached to media events.
+ * @param ad Updates to the properties for the ad context entity attached to media events during ad playback.
+ * @param adBreak Updates to the properties for the ad break context entity attached to media events during ad break playback.
+ */
+ fun track(
+ event: Event,
+ player: MediaPlayerEntity? = null,
+ ad: MediaAdEntity? = null,
+ adBreak: MediaAdBreakEntity? = null
+ )
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaAdBreakEntity.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaAdBreakEntity.kt
new file mode 100644
index 000000000..0f46f39c0
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaAdBreakEntity.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.entity
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
+
+/**
+ * Properties for the ad break context entity attached to media events during ad break playback.
+ * Entity schema: `iglu:com.snowplowanalytics.snowplow.media/ad_break/jsonschema/1-0-0`.
+ *
+ * @param breakId An identifier for the ad break.
+ * @param name Ad break name (e.g., pre-roll, mid-roll, and post-roll).
+ * @param breakType Type of ads within the break.
+ * @param podSize The number of ads to be played within the ad break.
+ */
+data class MediaAdBreakEntity @JvmOverloads constructor(
+ var breakId: String,
+ var name: String? = null,
+ var breakType: MediaAdBreakType? = null,
+ var podSize: Int? = null,
+) {
+ /**
+ * Playback time in seconds at the start of the ad break.
+ * Set automatically from the player entity.
+ */
+ var startTime: Double? = null
+
+ internal val entity: SelfDescribingJson
+ get() = SelfDescribingJson(
+ MediaSchemata.adBreakSchema,
+ mapOf(
+ "breakId" to breakId,
+ "name" to name,
+ "startTime" to startTime,
+ "breakType" to breakType?.toString(),
+ "podSize" to podSize,
+ )
+ .filterValues { it != null }
+ )
+
+ internal fun update(fromAdBreak: MediaAdBreakEntity) {
+ breakId = fromAdBreak.breakId
+ fromAdBreak.name?.let { name = it }
+ fromAdBreak.breakType?.let { breakType = it }
+ fromAdBreak.podSize?.let { podSize = it }
+ }
+
+ internal fun update(fromPlayer: MediaPlayerEntity) {
+ if (startTime == null) {
+ startTime = fromPlayer.currentTime ?: 0.0
+ }
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaAdBreakType.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaAdBreakType.kt
new file mode 100644
index 000000000..965afcd07
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaAdBreakType.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.entity
+
+/**
+ * Type of ads within the break.
+ */
+enum class MediaAdBreakType {
+ /// Take full control of the video for a period of time
+ Linear,
+ /// Run concurrently to the video
+ NonLinear,
+ /// Accompany the video but placed outside the player
+ Companion;
+
+ override fun toString(): String {
+ return when (this) {
+ Linear -> "linear"
+ NonLinear -> "nonlinear"
+ Companion -> "companion"
+ }
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaAdEntity.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaAdEntity.kt
new file mode 100644
index 000000000..363c454dd
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaAdEntity.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.entity
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
+
+/**
+ * Properties for the ad context entity attached to media events during ad playback.
+ * Entity schema: `iglu:com.snowplowanalytics.snowplow.media/ad/jsonschema/1-0-0`.
+ *
+ * @param adId Unique identifier for the ad.
+ * @param name Friendly name of the ad.
+ * @param creativeId The ID of the ad creative.
+ * @param podPosition The position of the ad within the ad break, starting with 1. It is automatically assigned by the tracker based on the tracked ad break start and ad start events.
+ * @param duration Length of the video ad in seconds.
+ * @param skippable Indicating whether skip controls are made available to the end user.
+ */
+data class MediaAdEntity @JvmOverloads constructor(
+ var adId: String,
+ var name: String? = null,
+ var creativeId: String? = null,
+ var podPosition: Int? = null,
+ var duration: Double? = null,
+ var skippable: Boolean? = null,
+) {
+ internal val entity: SelfDescribingJson
+ get() = SelfDescribingJson(
+ MediaSchemata.adSchema,
+ mapOf(
+ "adId" to adId,
+ "name" to name,
+ "creativeId" to creativeId,
+ "podPosition" to podPosition,
+ "duration" to duration,
+ "skippable" to skippable,
+ )
+ .filterValues { it != null }
+ )
+
+ internal fun update(fromAd: MediaAdEntity) {
+ adId = fromAd.adId
+ fromAd.name?.let { name = it }
+ fromAd.creativeId?.let { creativeId = it }
+ fromAd.podPosition?.let { podPosition = it }
+ fromAd.duration?.let { duration = it }
+ fromAd.skippable?.let { skippable = it }
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaPlayerEntity.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaPlayerEntity.kt
new file mode 100644
index 000000000..0f0bd414a
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaPlayerEntity.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.entity
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.payload.SelfDescribingJson
+
+/**
+ * Properties for the media player context entity attached to media events.
+ * Entity schema: `iglu:com.snowplowanalytics.snowplow.media/player/jsonschema/1-0-0`.
+ *
+ * @param currentTime The current playback time position within the media in seconds.
+ * @param duration Duration of the media in seconds.
+ * @param ended If playback of the media has ended.
+ * @param fullscreen Whether the video element is fullscreen.
+ * @param livestream Whether the media is a livestream.
+ * @param label Human readable name given to tracked media content.
+ * @param loop If the video should restart after ending.
+ * @param mediaType Type of media content.
+ * @param muted If the media element is muted.
+ * @param paused If the media element is paused.
+ * @param pictureInPicture Whether the video element is showing picture-in-picture.
+ * @param playerType Type of the media player (e.g., com.youtube-youtube, com.vimeo-vimeo, org.whatwg-media_element).
+ * @param playbackRate Playback rate (1 is normal).
+ * @param quality Quality level of the playback (e.g., 1080p, 720p).
+ * @param volume Volume percent (0 is muted, 100 is max).
+ */
+data class MediaPlayerEntity @JvmOverloads constructor(
+ var currentTime: Double? = null,
+ var duration: Double? = null,
+ var ended: Boolean? = null,
+ var fullscreen: Boolean? = null,
+ var livestream: Boolean? = null,
+ var label: String? = null,
+ var loop: Boolean? = null,
+ var mediaType: MediaType? = null,
+ var muted: Boolean? = null,
+ var paused: Boolean? = null,
+ var pictureInPicture: Boolean? = null,
+ var playerType: String? = null,
+ var playbackRate: Double? = null,
+ var quality: String? = null,
+ var volume: Int? = null
+) {
+ /** The percent of the way through the media (0 to 100) */
+ val percentProgress: Int?
+ get() {
+ return duration?.let { duration ->
+ return ((currentTime ?: 0.0) / duration * 100).toInt()
+ }
+ }
+
+ internal val entity: SelfDescribingJson
+ get() = SelfDescribingJson(
+ MediaSchemata.playerSchema,
+ mapOf(
+ "currentTime" to (currentTime ?: 0.0),
+ "duration" to duration,
+ "ended" to (ended ?: false),
+ "fullscreen" to fullscreen,
+ "livestream" to livestream,
+ "label" to label,
+ "loop" to loop,
+ "mediaType" to mediaType?.toString(),
+ "muted" to muted,
+ "paused" to (paused ?: true),
+ "pictureInPicture" to pictureInPicture,
+ "playerType" to playerType,
+ "playbackRate" to playbackRate,
+ "quality" to quality,
+ "volume" to volume
+ ).filter { it.value != null }
+ )
+
+ internal fun update(player: MediaPlayerEntity) {
+ player.currentTime?.let { currentTime = it }
+ player.duration?.let { duration = it }
+ player.ended?.let { ended = it }
+ player.fullscreen?.let { fullscreen = it }
+ player.livestream?.let { livestream = it }
+ player.label?.let { label = it }
+ player.loop?.let { loop = it }
+ player.mediaType?.let { mediaType = it }
+ player.muted?.let { muted = it }
+ player.paused?.let { paused = it }
+ player.pictureInPicture?.let { pictureInPicture = it }
+ player.playerType?.let { playerType = it }
+ player.playbackRate?.let { playbackRate = it }
+ player.quality?.let { quality = it }
+ player.volume?.let { volume = it }
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaType.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaType.kt
new file mode 100644
index 000000000..1abb22e8d
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/entity/MediaType.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.entity
+
+/** Type of media content. */
+enum class MediaType {
+ /// Video content
+ Video,
+ /// Audio content
+ Audio;
+
+ override fun toString(): String {
+ return when (this) {
+ Video -> "video"
+ Audio -> "audio"
+ }
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdBreakEndEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdBreakEndEvent.kt
new file mode 100644
index 000000000..3703dea8d
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdBreakEndEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event that signals the end of an ad break.
+ */
+class MediaAdBreakEndEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_break_end")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdBreakStartEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdBreakStartEvent.kt
new file mode 100644
index 000000000..e8286cad6
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdBreakStartEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event that signals the start of an ad break.
+ */
+class MediaAdBreakStartEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_break_start")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdClickEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdClickEvent.kt
new file mode 100644
index 000000000..a0e44bc1a
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdClickEvent.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when the user clicked on the ad.
+ *
+ * @param percentProgress The percentage of the ad that was played when the user clicked on it.
+ */
+class MediaAdClickEvent(var percentProgress: Int? = null) : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_click")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "percentProgress" to percentProgress
+ ).filterValues { it != null }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdCompleteEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdCompleteEvent.kt
new file mode 100644
index 000000000..ce7a37e49
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdCompleteEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event that signals the ad creative was played to the end at normal speed.
+ */
+class MediaAdCompleteEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_complete")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdFirstQuartileEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdFirstQuartileEvent.kt
new file mode 100644
index 000000000..63b1806bf
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdFirstQuartileEvent.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when 25% of ad is reached after continuous ad playback at normal speed.
+ */
+class MediaAdFirstQuartileEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_quartile")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "percentProgress" to 25,
+ )
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdMidpointEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdMidpointEvent.kt
new file mode 100644
index 000000000..c49653833
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdMidpointEvent.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when a midpoint of ad is reached after continuous ad playback at normal speed.
+ */
+class MediaAdMidpointEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_quartile")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "percentProgress" to 50,
+ )
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdPauseEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdPauseEvent.kt
new file mode 100644
index 000000000..47ffea7ef
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdPauseEvent.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when the user clicked the pause control and stopped the ad creative.
+ *
+ * @param percentProgress The percentage of the ad that was played when the user paused it.
+ */
+class MediaAdPauseEvent(var percentProgress: Int? = null) : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_pause")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "percentProgress" to percentProgress
+ ).filterValues { it != null }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdResumeEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdResumeEvent.kt
new file mode 100644
index 000000000..d7292dbd7
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdResumeEvent.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when the user resumed playing the ad creative after it had been stopped or paused.
+ *
+ * @param percentProgress The percentage of the ad that was played when the user resumed it.
+ */
+class MediaAdResumeEvent(var percentProgress: Int? = null) : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_resume")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "percentProgress" to percentProgress
+ ).filterValues { it != null }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdSkipEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdSkipEvent.kt
new file mode 100644
index 000000000..22be036bc
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdSkipEvent.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when the user activated a skip control to skip the ad creative.
+ *
+ * @param percentProgress The percentage of the ad that was played when the user skipped it.
+ */
+class MediaAdSkipEvent(var percentProgress: Int? = null) : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_skip")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "percentProgress" to percentProgress
+ ).filterValues { it != null }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdStartEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdStartEvent.kt
new file mode 100644
index 000000000..51f0c105d
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdStartEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event that signals the start of an ad.
+ */
+class MediaAdStartEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_start")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdThirdQuartileEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdThirdQuartileEvent.kt
new file mode 100644
index 000000000..8885ad323
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaAdThirdQuartileEvent.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when 75% of ad is reached after continuous ad playback at normal speed.
+ */
+class MediaAdThirdQuartileEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ad_quartile")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "percentProgress" to 75,
+ )
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaBufferEndEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaBufferEndEvent.kt
new file mode 100644
index 000000000..df7aff59d
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaBufferEndEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when the the player finishes buffering content and resumes playback.
+ */
+class MediaBufferEndEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("buffer_end")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaBufferStartEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaBufferStartEvent.kt
new file mode 100644
index 000000000..92e8f4ab4
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaBufferStartEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when the player goes into the buffering state and begins to buffer content.
+ */
+class MediaBufferStartEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("buffer_start")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaEndEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaEndEvent.kt
new file mode 100644
index 000000000..2cd848da1
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaEndEvent.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.core.media.event.MediaPlayerUpdatingEvent
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Media player event sent when playback stops when end of the media is reached or because no further data is available.
+ */
+class MediaEndEvent : AbstractSelfDescribing(), MediaPlayerUpdatingEvent {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("end")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+
+ override fun update(player: MediaPlayerEntity) {
+ player.ended = true
+ player.paused = true
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaErrorEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaErrorEvent.kt
new file mode 100644
index 000000000..f36f66330
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaErrorEvent.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event tracked when the resource could not be loaded due to an error.
+ *
+ * @param errorCode Error-identifying code for the playback issue. E.g. E522.
+ * @param errorName Name for the type of error that occurred in the playback. E.g. forbidden.
+ * @param errorDescription Longer description for the error that occurred in the playback.
+ */
+class MediaErrorEvent @JvmOverloads constructor(
+ var errorCode: String? = null,
+ var errorName: String? = null,
+ var errorDescription: String? = null
+) : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("error")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "errorCode" to errorCode,
+ "errorName" to errorName,
+ "errorDescription" to errorDescription,
+ ).filterValues { it != null }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaFullscreenChangeEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaFullscreenChangeEvent.kt
new file mode 100644
index 000000000..d3b905dda
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaFullscreenChangeEvent.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.core.media.event.MediaPlayerUpdatingEvent
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Media player event fired immediately after the browser switches into or out of full-screen mode.
+ *
+ * @param fullscreen Whether the video element is fullscreen after the change.
+ */
+class MediaFullscreenChangeEvent(
+ var fullscreen: Boolean
+) : AbstractSelfDescribing(), MediaPlayerUpdatingEvent {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("fullscreen_change")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "fullscreen" to fullscreen
+ )
+
+ override fun update(player: MediaPlayerEntity) {
+ player.fullscreen = fullscreen
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPauseEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPauseEvent.kt
new file mode 100644
index 000000000..34180f376
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPauseEvent.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.core.media.event.MediaPlayerUpdatingEvent
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Media player event sent when the user pauses the playback.
+ */
+class MediaPauseEvent : AbstractSelfDescribing(), MediaPlayerUpdatingEvent {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("pause")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+
+ override fun update(player: MediaPlayerEntity) {
+ player.paused = true
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPictureInPictureChangeEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPictureInPictureChangeEvent.kt
new file mode 100644
index 000000000..32f3c2c5c
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPictureInPictureChangeEvent.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.core.media.event.MediaPlayerUpdatingEvent
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Media player event fired immediately after the browser switches into or out of picture-in-picture mode.
+ *
+ * @param pictureInPicture Whether the video element is showing picture-in-picture after the change.
+ */
+class MediaPictureInPictureChangeEvent(
+ var pictureInPicture: Boolean
+) : AbstractSelfDescribing(), MediaPlayerUpdatingEvent {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("picture_in_picture_change")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "pictureInPicture" to pictureInPicture
+ )
+
+ override fun update(player: MediaPlayerEntity) {
+ player.pictureInPicture = pictureInPicture
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPlayEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPlayEvent.kt
new file mode 100644
index 000000000..657900395
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPlayEvent.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.core.media.event.MediaPlayerUpdatingEvent
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Media player event sent when the player changes state to playing from previously being paused.
+ */
+class MediaPlayEvent : AbstractSelfDescribing(), MediaPlayerUpdatingEvent {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("play")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+
+ override fun update(player: MediaPlayerEntity) {
+ player.paused = false
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPlaybackRateChangeEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPlaybackRateChangeEvent.kt
new file mode 100644
index 000000000..b90a53df7
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaPlaybackRateChangeEvent.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.core.media.event.MediaPlayerUpdatingEvent
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Media player event sent when the playback rate has changed.
+ *
+ * @param newRate Playback rate after the change (1 is normal).
+ * @param previousRate Playback rate before the change (1 is normal). If not set, the previous rate is taken from the last setting in media player.
+ */
+class MediaPlaybackRateChangeEvent @JvmOverloads constructor (
+ var newRate: Double,
+ var previousRate: Double? = null
+) : AbstractSelfDescribing(), MediaPlayerUpdatingEvent {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("playback_rate_change")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "previousRate" to previousRate,
+ "newRate" to newRate
+ ).filterValues { it != null }
+
+ override fun update(player: MediaPlayerEntity) {
+ if (previousRate == null) {
+ player.playbackRate?.let { previousRate = it }
+ }
+ player.playbackRate = newRate
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaQualityChangeEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaQualityChangeEvent.kt
new file mode 100644
index 000000000..27842c62f
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaQualityChangeEvent.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.core.media.event.MediaPlayerUpdatingEvent
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Media player event tracked when the video playback quality changes.
+ *
+ * @param newQuality Quality level after the change (e.g., 1080p).
+ * @param previousQuality Quality level before the change (e.g., 1080p). If not set, the previous quality is taken from the last setting in media player.
+ * @param bitrate The current bitrate in bits per second.
+ * @param framesPerSecond The current number of frames per second.
+ * @param automatic Whether the change was automatic or triggered by the user.
+ */
+class MediaQualityChangeEvent @JvmOverloads constructor (
+ var newQuality: String? = null,
+ var previousQuality: String? = null,
+ var bitrate: Int? = null,
+ var framesPerSecond: Int? = null,
+ var automatic: Boolean? = null
+) : AbstractSelfDescribing(), MediaPlayerUpdatingEvent {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("quality_change")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "previousQuality" to previousQuality,
+ "newQuality" to newQuality,
+ "bitrate" to bitrate,
+ "framesPerSecond" to framesPerSecond,
+ "automatic" to automatic
+ ).filterValues { it != null }
+
+ override fun update(player: MediaPlayerEntity) {
+ if (previousQuality == null) {
+ player.quality?.let { previousQuality = it }
+ }
+ player.quality = newQuality
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaReadyEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaReadyEvent.kt
new file mode 100644
index 000000000..b72e030df
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaReadyEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event fired when the media tracking is successfully attached to the player and can track events.
+ */
+class MediaReadyEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("ready")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaSeekEndEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaSeekEndEvent.kt
new file mode 100644
index 000000000..69719e067
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaSeekEndEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event sent when a seek operation completes.
+ */
+class MediaSeekEndEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("seek_end")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaSeekStartEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaSeekStartEvent.kt
new file mode 100644
index 000000000..9b96f81bf
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaSeekStartEvent.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+
+/**
+ * Media player event sent when a seek operation begins.
+ */
+class MediaSeekStartEvent : AbstractSelfDescribing() {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("seek_start")
+
+ override val dataPayload: Map
+ get() = emptyMap()
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaVolumeChangeEvent.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaVolumeChangeEvent.kt
new file mode 100644
index 000000000..a08c9586f
--- /dev/null
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/media/event/MediaVolumeChangeEvent.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+
+package com.snowplowanalytics.snowplow.media.event
+
+import com.snowplowanalytics.core.media.MediaSchemata
+import com.snowplowanalytics.core.media.event.MediaPlayerUpdatingEvent
+import com.snowplowanalytics.snowplow.event.AbstractSelfDescribing
+import com.snowplowanalytics.snowplow.media.entity.MediaPlayerEntity
+
+/**
+ * Media player event sent when the volume has changed.
+ *
+ * @param newVolume Volume percentage after the change.
+ * @param previousVolume Volume percentage before the change. If not set, the previous volume is taken from the last setting in media player.
+ */
+class MediaVolumeChangeEvent @JvmOverloads constructor (
+ var newVolume: Int,
+ var previousVolume: Int? = null,
+) : AbstractSelfDescribing(), MediaPlayerUpdatingEvent {
+ override val schema: String
+ get() = MediaSchemata.eventSchema("volume_change")
+
+ override val dataPayload: Map
+ get() = mapOf(
+ "previousVolume" to previousVolume,
+ "newVolume" to newVolume,
+ ).filterValues { it != null }
+
+ override fun update(player: MediaPlayerEntity) {
+ if (previousVolume == null) {
+ player.volume?.let { previousVolume = it }
+ }
+ player.volume = newVolume
+ }
+}
diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/network/OkHttpNetworkConnection.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/network/OkHttpNetworkConnection.kt
index b2dc7ba95..249dfccc4 100644
--- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/network/OkHttpNetworkConnection.kt
+++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/network/OkHttpNetworkConnection.kt
@@ -49,6 +49,7 @@ class OkHttpNetworkConnection private constructor(builder: OkHttpNetworkConnecti
private val emitTimeout: Int
private val customPostPath: String?
private val serverAnonymisation: Boolean
+ private val requestHeaders: Map?
private var client: OkHttpClient? = null
private val uriBuilder: Uri.Builder
override val uri: Uri
@@ -69,6 +70,7 @@ class OkHttpNetworkConnection private constructor(builder: OkHttpNetworkConnecti
var cookieJar: CookieJar? = null // Optional
var customPostPath: String? = null //Optional
var serverAnonymisation = EmitterDefaults.serverAnonymisation // Optional
+ var requestHeaders: Map? = null // Optional
/**
* GET or POST.
@@ -166,6 +168,14 @@ class OkHttpNetworkConnection private constructor(builder: OkHttpNetworkConnecti
return this
}
+ /**
+ * A map of custom HTTP headers to add to the request.
+ */
+ fun requestHeaders(requestHeaders: Map?): OkHttpNetworkConnectionBuilder {
+ this.requestHeaders = requestHeaders
+ return this
+ }
+
/**
* Creates a new OkHttpNetworkConnection
*
@@ -198,6 +208,7 @@ class OkHttpNetworkConnection private constructor(builder: OkHttpNetworkConnecti
emitTimeout = builder.emitTimeout
customPostPath = builder.customPostPath
serverAnonymisation = builder.serverAnonymisation
+ requestHeaders = builder.requestHeaders
val tlsArguments = TLSArguments(builder.tlsVersions)
uriBuilder = Uri.parse(networkUri).buildUpon()
@@ -298,6 +309,11 @@ class OkHttpNetworkConnection private constructor(builder: OkHttpNetworkConnecti
if (serverAnonymisation) {
builder.header("SP-Anonymous", "*")
}
+ requestHeaders?.let {
+ it.forEach { (key, value) ->
+ builder.header(key, value)
+ }
+ }
return builder.build()
}
@@ -319,6 +335,11 @@ class OkHttpNetworkConnection private constructor(builder: OkHttpNetworkConnecti
if (serverAnonymisation) {
builder.header("SP-Anonymous", "*")
}
+ requestHeaders?.let {
+ it.forEach { (key, value) ->
+ builder.header(key, value)
+ }
+ }
return builder.build()
}