From 03dd29705c5eb1214c17bc61ac9aa9c7672eafcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 28 Feb 2024 15:18:50 +0100 Subject: [PATCH 1/2] Fix createTracker call hanging for 10 seconds if run on a background thread (close #620) --- snowplow-demo-kotlin/build.gradle | 1 + .../src/main/AndroidManifest.xml | 5 ++ .../internal/tracker/PlatformContextTest.kt | 34 +++++--- .../core/tracker/PlatformContext.kt | 86 +++++++------------ .../core/utils/DeviceInfoMonitor.kt | 4 +- 5 files changed, 60 insertions(+), 70 deletions(-) diff --git a/snowplow-demo-kotlin/build.gradle b/snowplow-demo-kotlin/build.gradle index 90cbc86b4..baaee1a9d 100644 --- a/snowplow-demo-kotlin/build.gradle +++ b/snowplow-demo-kotlin/build.gradle @@ -43,4 +43,5 @@ dependencies { implementation 'androidx.browser:browser:1.5.0' implementation 'com.google.android.gms:play-services-appset:16.0.2' implementation "com.android.installreferrer:installreferrer:2.2" + implementation "com.google.android.gms:play-services-ads:22.6.0" } diff --git a/snowplow-demo-kotlin/src/main/AndroidManifest.xml b/snowplow-demo-kotlin/src/main/AndroidManifest.xml index 63d5ec095..70a9010b6 100644 --- a/snowplow-demo-kotlin/src/main/AndroidManifest.xml +++ b/snowplow-demo-kotlin/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + + 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 a96a87c45..f7e9ba6a4 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 @@ -49,7 +49,6 @@ class PlatformContextTest { fun addsAllMockedInfo() { val deviceInfoMonitor = MockDeviceInfoMonitor() val platformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) - Thread.sleep(100) // sleep in order to fetch the app set properties val sdj = platformContext.getMobileContext(false) val sdjMap = sdj!!.map val sdjData = sdjMap["data"] as Map<*, *>? @@ -79,6 +78,7 @@ class PlatformContextTest { fun updatesMobileInfo() { val deviceInfoMonitor = MockDeviceInfoMonitor() val platformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) + platformContext.getMobileContext(false) Assert.assertEquals( 1, deviceInfoMonitor.getMethodAccessCount("getSystemAvailableMemory").toLong() @@ -98,10 +98,25 @@ class PlatformContextTest { ) } + @Test + fun doesntFetchPropertiesIfNotRequested() { + val deviceInfoMonitor = MockDeviceInfoMonitor() + PlatformContext(1000, 0, deviceInfoMonitor, context = context) + Assert.assertEquals( + 0, + deviceInfoMonitor.getMethodAccessCount("getSystemAvailableMemory").toLong() + ) + Assert.assertEquals( + 0, + deviceInfoMonitor.getMethodAccessCount("getBatteryStateAndLevel").toLong() + ) + } + @Test fun doesntUpdateMobileInfoWithinUpdateWindow() { val deviceInfoMonitor = MockDeviceInfoMonitor() val platformContext = PlatformContext(1000, 0, deviceInfoMonitor, context = context) + platformContext.getMobileContext(false) Assert.assertEquals( 1, deviceInfoMonitor.getMethodAccessCount("getSystemAvailableMemory").toLong() @@ -125,6 +140,7 @@ class PlatformContextTest { fun updatesNetworkInfo() { val deviceInfoMonitor = MockDeviceInfoMonitor() val platformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) + platformContext.getMobileContext(false) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getNetworkType").toLong()) Assert.assertEquals( 1, @@ -142,6 +158,7 @@ class PlatformContextTest { fun doesntUpdateNetworkInfoWithinUpdateWindow() { val deviceInfoMonitor = MockDeviceInfoMonitor() val platformContext = PlatformContext(0, 1000, deviceInfoMonitor, context = context) + platformContext.getMobileContext(false) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getNetworkType").toLong()) Assert.assertEquals( 1, @@ -159,6 +176,7 @@ class PlatformContextTest { fun doesntUpdateNonEphemeralInfo() { val deviceInfoMonitor = MockDeviceInfoMonitor() val platformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) + platformContext.getMobileContext(false) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getOsType").toLong()) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getTotalStorage").toLong()) platformContext.getMobileContext(false) @@ -170,22 +188,10 @@ class PlatformContextTest { fun doesntUpdateIdfaIfNotNull() { val deviceInfoMonitor = MockDeviceInfoMonitor() val platformContext = PlatformContext(0, 1, deviceInfoMonitor, context = context) - Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getAndroidIdfa").toLong()) platformContext.getMobileContext(false) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getAndroidIdfa").toLong()) - } - - @Test - fun updatesIdfaIfEmptyOrNull() { - val deviceInfoMonitor = MockDeviceInfoMonitor() - deviceInfoMonitor.customIdfa = "" - val platformContext = PlatformContext(0, 1, deviceInfoMonitor, context = context) - Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getAndroidIdfa").toLong()) - deviceInfoMonitor.customIdfa = null - platformContext.getMobileContext(false) - Assert.assertEquals(2, deviceInfoMonitor.getMethodAccessCount("getAndroidIdfa").toLong()) platformContext.getMobileContext(false) - Assert.assertEquals(3, deviceInfoMonitor.getMethodAccessCount("getAndroidIdfa").toLong()) + Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getAndroidIdfa").toLong()) } @Test 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 a11a85f82..2419ed929 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 @@ -15,7 +15,6 @@ package com.snowplowanalytics.core.tracker import android.content.Context import com.snowplowanalytics.core.constants.Parameters import com.snowplowanalytics.core.constants.TrackerConstants -import com.snowplowanalytics.core.emitter.Executor import com.snowplowanalytics.core.utils.DeviceInfoMonitor import com.snowplowanalytics.core.utils.Util.addToMap import com.snowplowanalytics.core.utils.Util.mapHasKeys @@ -43,15 +42,12 @@ class PlatformContext( private val context: Context, ) { private val pairs: MutableMap = HashMap() + private var initializedPlatformDict = false private var lastUpdatedEphemeralPlatformDict: Long = 0 private var lastUpdatedEphemeralNetworkDict: Long = 0 - - init { - setPlatformDict() - } fun getMobileContext(userAnonymisation: Boolean): SelfDescribingJson? { - updateEphemeralDictsIfNecessary() + update() // If does not contain the required properties, return null if (!mapHasKeys( @@ -76,7 +72,11 @@ class PlatformContext( // --- PRIVATE @Synchronized - private fun updateEphemeralDictsIfNecessary() { + private fun update() { + if (!initializedPlatformDict) { + setPlatformDict() + } + val now = System.currentTimeMillis() if (now - lastUpdatedEphemeralPlatformDict >= platformDictUpdateFrequency) { setEphemeralPlatformDict() @@ -122,26 +122,25 @@ class PlatformContext( if (shouldTrack(PlatformContextProperty.LANGUAGE)) { addToMap(Parameters.MOBILE_LANGUAGE, (fromRetrieverOr(retriever.language) { deviceInfoMonitor.language })?.take(8), pairs) } + // IDFA + if (shouldTrack(PlatformContextProperty.ANDROID_IDFA)) { + addToMap( + Parameters.ANDROID_IDFA, + fromRetrieverOr(retriever.androidIdfa) { + deviceInfoMonitor.getAndroidIdfa(context) + }, + pairs + ) + } - setEphemeralPlatformDict() - setEphemeralNetworkDict() setAppSetId() + + initializedPlatformDict = true } private fun setEphemeralPlatformDict() { lastUpdatedEphemeralPlatformDict = System.currentTimeMillis() - // IDFA - if (shouldTrack(PlatformContextProperty.ANDROID_IDFA)) { - val currentIdfa = pairs[Parameters.ANDROID_IDFA] - if (currentIdfa == null || currentIdfa.toString().isEmpty()) { - addToMap( - Parameters.ANDROID_IDFA, - fromRetrieverOr(retriever.androidIdfa) { deviceInfoMonitor.getAndroidIdfa(context) }, - pairs - ) - } - } // Battery val trackBatState = shouldTrack(PlatformContextProperty.BATTERY_STATE) val trackBatLevel = shouldTrack(PlatformContextProperty.BATTERY_LEVEL) @@ -194,48 +193,25 @@ class PlatformContext( /** * Sets the app set information. - * The info has to be read on a background thread which often means that the first few - * tracked events will miss the info. To prevent that happening on the second start-up - * of the app, the info is saved in general prefs and read from there. */ private fun setAppSetId() { val trackId = shouldTrack(PlatformContextProperty.APP_SET_ID) val trackScope = shouldTrack(PlatformContextProperty.APP_SET_ID_SCOPE) if (!trackId && !trackScope) { return } - val generalPref = context.getSharedPreferences( - TrackerConstants.SNOWPLOW_GENERAL_VARS, - Context.MODE_PRIVATE - ) - val appSetId = fromRetrieverOr(retriever.appSetId) { generalPref.getString(Parameters.APP_SET_ID, null) } - val appSetIdScope = fromRetrieverOr(retriever.appSetIdScope) { generalPref.getString(Parameters.APP_SET_ID_SCOPE, null) } - - if (appSetId != null && appSetIdScope != null) { - if (trackId) { addToMap(Parameters.APP_SET_ID, appSetId, pairs) } - if (trackScope) { addToMap(Parameters.APP_SET_ID_SCOPE, appSetIdScope, pairs) } + if (retriever.appSetId != null && retriever.appSetIdScope != null) { + if (trackId) { addToMap(Parameters.APP_SET_ID, retriever.appSetId?.invoke(), pairs) } + if (trackScope) { addToMap(Parameters.APP_SET_ID_SCOPE, retriever.appSetIdScope?.invoke(), pairs) } } else { - Executor.execute(TAG) { - val preferences = generalPref.edit() - var edited = false - - val appSetIdAndScope = deviceInfoMonitor.getAppSetIdAndScope(context) - val id = fromRetrieverOr(retriever.appSetId) { - val id = appSetIdAndScope?.first - preferences.putString(Parameters.APP_SET_ID, id) - edited = true - id - } - val scope = fromRetrieverOr(retriever.appSetIdScope) { - val scope = appSetIdAndScope?.second - preferences.putString(Parameters.APP_SET_ID_SCOPE, scope) - edited = true - scope - } - - if (trackId) { addToMap(Parameters.APP_SET_ID, id, pairs) } - if (trackScope) { addToMap(Parameters.APP_SET_ID_SCOPE, scope, pairs) } - - if (edited) { preferences.apply() } + val appSetIdAndScope = deviceInfoMonitor.getAppSetIdAndScope(context) + + if (trackId) { + val id = fromRetrieverOr(retriever.appSetId) { appSetIdAndScope?.first } + addToMap(Parameters.APP_SET_ID, id, pairs) + } + if (trackScope) { + val scope = fromRetrieverOr(retriever.appSetIdScope) { appSetIdAndScope?.second } + addToMap(Parameters.APP_SET_ID_SCOPE, scope, pairs) } } } diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/utils/DeviceInfoMonitor.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/utils/DeviceInfoMonitor.kt index be4a3aab9..55ea9e19e 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/core/utils/DeviceInfoMonitor.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/core/utils/DeviceInfoMonitor.kt @@ -49,12 +49,14 @@ open class DeviceInfoMonitor { /** * The function that actually fetches the Advertising ID. - * - If called from the UI Thread will throw an Exception + * Can't be called on the main UI thread. * * @param context the android context * @return an empty string if limited tracking is on otherwise the advertising id or null */ open fun getAndroidIdfa(context: Context): String? { + if (Looper.myLooper() == Looper.getMainLooper()) { return null } + return try { val advertisingInfoObject = invokeStaticMethod( "com.google.android.gms.ads.identifier.AdvertisingIdClient", From be0ecd8dbb9c002e506648ae9a8165083a30facb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 28 Feb 2024 15:26:59 +0100 Subject: [PATCH 2/2] Prepare for 6.0.2 release --- CHANGELOG | 4 ++++ VERSION | 2 +- build.gradle | 2 +- gradle.properties | 2 +- .../snowplow/event/ApplicationInstallEventTest.kt | 2 ++ 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 92fcbe49e..026fa8223 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +Version 6.0.2 (2024-02-28) +-------------------------- +Fix createTracker call hanging for 10 seconds if run on a background thread (#620) + Version 6.0.1 (2024-02-14) -------------------------- Fix wrong screen entity info assigned for screen end event and in case events are tracked right before screen view (#673) diff --git a/VERSION b/VERSION index 5fe607230..9b9a24420 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.0.1 +6.0.2 diff --git a/build.gradle b/build.gradle index c9c25e84b..a957a6ea9 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ plugins { subprojects { group = 'com.snowplowanalytics' - version = '6.0.1' + version = '6.0.2' repositories { google() maven { diff --git a/gradle.properties b/gradle.properties index 88c8c9605..c2eab7516 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=6.0.1 +VERSION_NAME=6.0.2 POM_NAME=snowplow-android-tracker POM_PACKAGING=aar 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 eb09f15b6..a776f6d41 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 @@ -16,6 +16,7 @@ import android.content.Context import androidx.preference.PreferenceManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.snowplowanalytics.core.emitter.Executor import com.snowplowanalytics.snowplow.Snowplow.createTracker import com.snowplowanalytics.snowplow.configuration.* import com.snowplowanalytics.snowplow.controller.TrackerController @@ -32,6 +33,7 @@ class ApplicationInstallEventTest { @Before fun setUp() { cleanSharedPreferences() + Executor.shutdown() } // Tests