From 9ae3a3f95ef24c40d3a1aafe0938418e5d23098d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Sun, 28 Jan 2024 11:35:39 +0100 Subject: [PATCH 1/2] Add an option to override platform context properties --- .../internal/tracker/PlatformContextTest.kt | 93 +++++++++++--- .../internal/tracker/StateManagerTest.kt | 6 +- .../snowplow/internal/tracker/TrackerTest.kt | 18 +-- .../snowplow/tracker/LoggingTest.kt | 12 +- .../snowplow/tracker/SessionTest.kt | 10 +- .../tracker/integration/EventSendingTest.kt | 4 +- .../core/tracker/PlatformContext.kt | 117 ++++++++++-------- .../core/tracker/ServiceProvider.kt | 13 +- .../snowplowanalytics/core/tracker/Tracker.kt | 10 +- .../configuration/TrackerConfiguration.kt | 21 +++- .../tracker/PlatformContextRetriever.kt | 78 ++++++++++++ 11 files changed, 274 insertions(+), 108 deletions(-) create mode 100644 snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/PlatformContextRetriever.kt 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 676a7e397..381c38c6f 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 @@ -19,6 +19,7 @@ import com.snowplowanalytics.core.constants.Parameters import com.snowplowanalytics.core.constants.TrackerConstants import com.snowplowanalytics.core.tracker.PlatformContext import com.snowplowanalytics.snowplow.configuration.PlatformContextProperty +import com.snowplowanalytics.snowplow.tracker.PlatformContextRetriever import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -29,7 +30,7 @@ class PlatformContextTest { // --- TESTS @Test fun addsNotMockedMobileContext() { - val platformContext = PlatformContext(null, context) + val platformContext = PlatformContext(context = context) val sdj = platformContext.getMobileContext(false) Assert.assertNotNull(sdj) val sdjMap = sdj!!.map @@ -47,7 +48,7 @@ class PlatformContextTest { @Test fun addsAllMockedInfo() { val deviceInfoMonitor = MockDeviceInfoMonitor() - val platformContext = PlatformContext(0, 0, deviceInfoMonitor, null, context) + 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 @@ -77,7 +78,7 @@ class PlatformContextTest { @Test fun updatesMobileInfo() { val deviceInfoMonitor = MockDeviceInfoMonitor() - val platformContext = PlatformContext(0, 0, deviceInfoMonitor, null, context) + val platformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) Assert.assertEquals( 1, deviceInfoMonitor.getMethodAccessCount("getSystemAvailableMemory").toLong() @@ -100,7 +101,7 @@ class PlatformContextTest { @Test fun doesntUpdateMobileInfoWithinUpdateWindow() { val deviceInfoMonitor = MockDeviceInfoMonitor() - val platformContext = PlatformContext(1000, 0, deviceInfoMonitor, null, context) + val platformContext = PlatformContext(1000, 0, deviceInfoMonitor, context = context) Assert.assertEquals( 1, deviceInfoMonitor.getMethodAccessCount("getSystemAvailableMemory").toLong() @@ -123,7 +124,7 @@ class PlatformContextTest { @Test fun updatesNetworkInfo() { val deviceInfoMonitor = MockDeviceInfoMonitor() - val platformContext = PlatformContext(0, 0, deviceInfoMonitor, null, context) + val platformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getNetworkType").toLong()) Assert.assertEquals( 1, @@ -140,7 +141,7 @@ class PlatformContextTest { @Test fun doesntUpdateNetworkInfoWithinUpdateWindow() { val deviceInfoMonitor = MockDeviceInfoMonitor() - val platformContext = PlatformContext(0, 1000, deviceInfoMonitor, null, context) + val platformContext = PlatformContext(0, 1000, deviceInfoMonitor, context = context) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getNetworkType").toLong()) Assert.assertEquals( 1, @@ -157,7 +158,7 @@ class PlatformContextTest { @Test fun doesntUpdateNonEphemeralInfo() { val deviceInfoMonitor = MockDeviceInfoMonitor() - val platformContext = PlatformContext(0, 0, deviceInfoMonitor, null, context) + val platformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getOsType").toLong()) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getTotalStorage").toLong()) platformContext.getMobileContext(false) @@ -168,7 +169,7 @@ class PlatformContextTest { @Test fun doesntUpdateIdfaIfNotNull() { val deviceInfoMonitor = MockDeviceInfoMonitor() - val platformContext = PlatformContext(0, 1, deviceInfoMonitor, null, context) + 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()) @@ -178,7 +179,7 @@ class PlatformContextTest { fun updatesIdfaIfEmptyOrNull() { val deviceInfoMonitor = MockDeviceInfoMonitor() deviceInfoMonitor.customIdfa = "" - val platformContext = PlatformContext(0, 1, deviceInfoMonitor, null, context) + val platformContext = PlatformContext(0, 1, deviceInfoMonitor, context = context) Assert.assertEquals(1, deviceInfoMonitor.getMethodAccessCount("getAndroidIdfa").toLong()) deviceInfoMonitor.customIdfa = null platformContext.getMobileContext(false) @@ -190,7 +191,7 @@ class PlatformContextTest { @Test fun anonymisesUserIdentifiers() { val deviceInfoMonitor = MockDeviceInfoMonitor() - val platformContext = PlatformContext(0, 0, deviceInfoMonitor, null, context) + val platformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) val sdj = platformContext.getMobileContext(true) val sdjMap = sdj!!.map val sdjData = sdjMap["data"] as Map<*, *>? @@ -201,16 +202,16 @@ class PlatformContextTest { @Test fun readsAppSetInfoSynchronouslyFromGeneralPrefsSecondTime() { val deviceInfoMonitor = MockDeviceInfoMonitor() - PlatformContext(0, 0, deviceInfoMonitor, null, context) + PlatformContext(0, 0, deviceInfoMonitor, context = context) Thread.sleep(100) - val secondPlatformContext = PlatformContext(0, 0, deviceInfoMonitor, null, context) + val secondPlatformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) val sdj = secondPlatformContext.getMobileContext(true) val sdjData = sdj!!.map["data"] as Map<*, *>? Assert.assertEquals("XXX", sdjData!![Parameters.APP_SET_ID]) - Assert.assertEquals("app", sdjData!![Parameters.APP_SET_ID_SCOPE]) + Assert.assertEquals("app", sdjData[Parameters.APP_SET_ID_SCOPE]) } @Test @@ -219,7 +220,7 @@ class PlatformContextTest { val platformContext = PlatformContext( 0, 0, deviceInfoMonitor, listOf(PlatformContextProperty.ANDROID_IDFA, PlatformContextProperty.BATTERY_LEVEL), - context + context = context ) val sdj = platformContext.getMobileContext(false) @@ -238,7 +239,7 @@ class PlatformContextTest { fun truncatesLanguageToMax8Chars() { val deviceInfoMonitor = MockDeviceInfoMonitor() deviceInfoMonitor.language = "1234567890" - val platformContext = PlatformContext(0, 0, deviceInfoMonitor, null, context) + val platformContext = PlatformContext(0, 0, deviceInfoMonitor, context = context) val sdj = platformContext.getMobileContext(false) Assert.assertNotNull(sdj) @@ -254,7 +255,7 @@ class PlatformContextTest { // set locale to an ISO-639 invalid 2-letter code Locale.setDefault(Locale("dk", "example")) - val platformContext = PlatformContext(null, context) + val platformContext = PlatformContext(context = context) val sdj = platformContext.getMobileContext(false) Assert.assertNotNull(sdj) val sdjData = sdj!!.map["data"] as Map<*, *> @@ -267,9 +268,9 @@ class PlatformContextTest { @Test fun doesntSetTheNetworkTechIfNotRequested() { - val platformContext = PlatformContext(listOf( + val platformContext = PlatformContext(properties = listOf( PlatformContextProperty.NETWORK_TYPE - ), context) + ), context = context) val sdj = platformContext.getMobileContext(false) Assert.assertNotNull(sdj) val sdjData = sdj!!.map["data"] as Map<*, *> @@ -278,6 +279,62 @@ class PlatformContextTest { Assert.assertFalse(sdjData.containsKey(Parameters.NETWORK_TECHNOLOGY)) } + @Test + fun PlatformContextRetrieverOverridesProperties() { + val retriever = PlatformContextRetriever( + osType = { "r1" }, + osVersion = { "r2" }, + deviceVendor = { "r3" }, + deviceModel = { "r4" }, + carrier = { "r5" }, + networkType = { "r6" }, + networkTechnology = { "r7" }, + androidIdfa = { "r8" }, + availableStorage = { 100 }, + totalStorage = { 101 }, + physicalMemory = { 102 }, + systemAvailableMemory = { 103 }, + batteryLevel = { 104 }, + batteryState = { "r9" }, + isPortrait = { false }, + resolution = { "r10" }, + scale = { 105f }, + language = { "r11" }, + appSetId = { "r12" }, + appSetIdScope = { "r13" }, + ) + val platformContext = PlatformContext( + retriever = retriever, + context = context + ) + Thread.sleep(100) + + val sdj = platformContext.getMobileContext(false) + Assert.assertNotNull(sdj) + val sdjData = sdj!!.map["data"] as Map<*, *> + + Assert.assertEquals("r1", sdjData[Parameters.OS_TYPE]) + Assert.assertEquals("r2", sdjData[Parameters.OS_VERSION]) + Assert.assertEquals("r3", sdjData[Parameters.DEVICE_MANUFACTURER]) + Assert.assertEquals("r4", sdjData[Parameters.DEVICE_MODEL]) + Assert.assertEquals("r5", sdjData[Parameters.CARRIER]) + Assert.assertEquals("r6", sdjData[Parameters.NETWORK_TYPE]) + Assert.assertEquals("r7", sdjData[Parameters.NETWORK_TECHNOLOGY]) + Assert.assertEquals("r8", sdjData[Parameters.ANDROID_IDFA]) + Assert.assertEquals(100L, sdjData[Parameters.AVAILABLE_STORAGE]) + Assert.assertEquals(101L, sdjData[Parameters.TOTAL_STORAGE]) + Assert.assertEquals(102L, sdjData[Parameters.PHYSICAL_MEMORY]) + Assert.assertEquals(103L, sdjData[Parameters.SYSTEM_AVAILABLE_MEMORY]) + Assert.assertEquals(104, sdjData[Parameters.BATTERY_LEVEL]) + Assert.assertEquals("r9", sdjData[Parameters.BATTERY_STATE]) + Assert.assertEquals(false, sdjData[Parameters.IS_PORTRAIT]) + Assert.assertEquals("r10", sdjData[Parameters.MOBILE_RESOLUTION]) + Assert.assertEquals(105f, sdjData[Parameters.MOBILE_SCALE]) + Assert.assertEquals("r11", sdjData[Parameters.MOBILE_LANGUAGE]) + Assert.assertEquals("r12", sdjData[Parameters.APP_SET_ID]) + Assert.assertEquals("r13", sdjData[Parameters.APP_SET_ID_SCOPE]) + } + // --- PRIVATE private val context: Context get() = InstrumentationRegistry.getInstrumentation().targetContext diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/StateManagerTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/StateManagerTest.kt index 050c8253b..02c944d6a 100644 --- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/StateManagerTest.kt +++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/internal/tracker/StateManagerTest.kt @@ -128,7 +128,7 @@ class StateManagerTest { tracker.base64Encoded = false tracker.logLevel = LogLevel.VERBOSE } - val tracker = Tracker(emitter, "namespace", "appId", null, context, trackerBuilder) + val tracker = Tracker(emitter, "namespace", "appId", context = context, builder = trackerBuilder) // Send events tracker.track(Timing("category", "variable", 123)) @@ -221,7 +221,7 @@ class StateManagerTest { tracker.base64Encoded = false tracker.logLevel = LogLevel.VERBOSE } - val tracker = Tracker(emitter, "namespace", "appId", null, context, trackerBuilder) + val tracker = Tracker(emitter, "namespace", "appId", context = context, builder = trackerBuilder) // Send events tracker.track(Timing("category", "variable", 123)) @@ -295,7 +295,7 @@ class StateManagerTest { tracker.sessionContext = false tracker.platformContextEnabled = false } - val tracker = Tracker(emitter, "namespace", "appId", null, context, trackerBuilder) + val tracker = Tracker(emitter, "namespace", "appId", context = context, builder = trackerBuilder) // Send events tracker.track(Timing("category", "variable", 123)) 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 7070be37c..b82f06b47 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 @@ -102,7 +102,7 @@ class TrackerTest { tracker.installAutotracking = installTracking tracker.applicationContext = true } - Companion.tracker = Tracker(emitter, "myNamespace", "myAppId", null, context, trackerBuilder) + Companion.tracker = Tracker(emitter, "myNamespace", "myAppId", context = context, builder = trackerBuilder) return Companion.tracker } @@ -203,7 +203,7 @@ class TrackerTest { tracker.screenViewAutotracking = false } Companion.tracker = - Tracker(emitter!!, namespace, "testTrackWithNoContext", null, context, trackerBuilder) + Tracker(emitter!!, namespace, "testTrackWithNoContext", context = context, builder = trackerBuilder) val eventStore = emitter.eventStore val isClean = eventStore.removeAllEvents() Log.i("testTrackSelfDescribingEvent", "EventStore clean: $isClean") @@ -265,7 +265,7 @@ class TrackerTest { tracker.screenViewAutotracking = false } Companion.tracker = - Tracker(emitter!!, namespace, "testTrackWithNoContext", null, context, trackerBuilder) + Tracker(emitter!!, namespace, "testTrackWithNoContext", context = context, builder = trackerBuilder) Log.i("testTrackWithNoContext", "Send ScreenView event") Companion.tracker!!.track(ScreenView("name")) @@ -317,7 +317,7 @@ class TrackerTest { tracker.exceptionAutotracking = false tracker.screenViewAutotracking = false } - Companion.tracker = Tracker(emitter, namespace, "myAppId", null, context, trackerBuilder) + Companion.tracker = Tracker(emitter, namespace, "myAppId", context = context, builder = trackerBuilder) Companion.tracker!!.pauseEventTracking() val eventId = Companion.tracker!!.track(ScreenView("name")) Assert.assertNull(eventId) @@ -351,7 +351,7 @@ class TrackerTest { tracker.foregroundTimeout = 5 tracker.backgroundTimeout = 5 } - Companion.tracker = Tracker(emitter, namespace, "myAppId", null, context, trackerBuilder) + Companion.tracker = Tracker(emitter, namespace, "myAppId", context = context, builder = trackerBuilder) Assert.assertNotNull(Companion.tracker!!.session) Companion.tracker!!.resumeSessionChecking() Thread.sleep(2000) @@ -380,7 +380,7 @@ class TrackerTest { tracker.foregroundTimeout = 5 tracker.backgroundTimeout = 5 } - Companion.tracker = Tracker(emitter, namespace, "myAppId", null, context, trackerBuilder) + Companion.tracker = Tracker(emitter, namespace, "myAppId", context = context, builder = trackerBuilder) val screenState = Companion.tracker!!.getScreenState() Assert.assertNotNull(screenState) var screenStateMapWrapper: Map = screenState!!.getCurrentScreen(true).map @@ -423,7 +423,7 @@ class TrackerTest { tracker.exceptionAutotracking = true tracker.screenViewAutotracking = false } - Companion.tracker = Tracker(emitter, namespace, "myAppId", null, context, trackerBuilder) + Companion.tracker = Tracker(emitter, namespace, "myAppId", context = context, builder = trackerBuilder) Assert.assertTrue(Companion.tracker!!.exceptionAutotracking) Assert.assertEquals( ExceptionHandler::class.java, @@ -451,7 +451,7 @@ class TrackerTest { tracker.exceptionAutotracking = false tracker.screenViewAutotracking = false } - Companion.tracker = Tracker(emitter, namespace, "myAppId", null, context, trackerBuilder) + Companion.tracker = Tracker(emitter, namespace, "myAppId", context = context, builder = trackerBuilder) val handler1 = ExceptionHandler() Thread.setDefaultUncaughtExceptionHandler(handler1) Assert.assertEquals( @@ -481,7 +481,7 @@ class TrackerTest { tracker.foregroundTimeout = 5 tracker.backgroundTimeout = 5 } - Companion.tracker = Tracker(emitter, "ns", "myAppId", null, context, trackerBuilder) + Companion.tracker = Tracker(emitter, "ns", "myAppId", context = context, builder = trackerBuilder) Companion.tracker!!.track(Structured("c", "a")) val sessionIdStart = Companion.tracker!!.session!!.state!!.sessionId diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/LoggingTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/LoggingTest.kt index aa711ccb6..656bbf177 100644 --- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/LoggingTest.kt +++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/LoggingTest.kt @@ -72,8 +72,8 @@ class LoggingTest { "namespace", "myAppId", null, - ApplicationProvider.getApplicationContext(), - trackerBuilder + context = ApplicationProvider.getApplicationContext(), + builder = trackerBuilder ) Assert.assertTrue(mockLoggerDelegate!!.capturedLogs.contains("Session checking has been resumed. (debug)")) Assert.assertTrue(mockLoggerDelegate!!.capturedLogs.contains("Tracker created successfully. (verbose)")) @@ -107,8 +107,8 @@ class LoggingTest { "namespace", "myAppId", null, - ApplicationProvider.getApplicationContext(), - trackerBuilder + context = ApplicationProvider.getApplicationContext(), + builder = trackerBuilder ) Assert.assertTrue(mockLoggerDelegate!!.capturedLogs.contains("Session checking has been resumed. (debug)")) Assert.assertFalse(mockLoggerDelegate!!.capturedLogs.contains("Tracker created successfully. (verbose)")) @@ -141,8 +141,8 @@ class LoggingTest { "namespace", "myAppId", null, - ApplicationProvider.getApplicationContext(), - trackerBuilder + context = ApplicationProvider.getApplicationContext(), + builder = trackerBuilder ) Assert.assertFalse(mockLoggerDelegate!!.capturedLogs.contains("Session checking has been resumed. (debug)")) Assert.assertFalse(mockLoggerDelegate!!.capturedLogs.contains("Tracker created successfully. (verbose)")) diff --git a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/SessionTest.kt b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/SessionTest.kt index 6dd7d2675..015625ba5 100644 --- a/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/SessionTest.kt +++ b/snowplow-tracker/src/androidTest/java/com/snowplowanalytics/snowplow/tracker/SessionTest.kt @@ -218,7 +218,7 @@ class SessionTest { tracker.foregroundTimeout = 100 tracker.backgroundTimeout = 2 } - val tracker = Tracker(emitter, "tracker", "app", null, context, trackerBuilder) + val tracker = Tracker(emitter, "tracker", "app", context = context, builder = trackerBuilder) val session = tracker.session getSessionContext(session, "event_1", timestamp, false) @@ -257,7 +257,7 @@ class SessionTest { tracker.foregroundTimeout = 100 tracker.backgroundTimeout = 2 } - val tracker = Tracker(emitter, "tracker", "app", null, context, trackerBuilder) + val tracker = Tracker(emitter, "tracker", "app", context = context, builder = trackerBuilder) val session = tracker.session getSessionContext(session, "event_1", timestamp, false) var sessionState = session!!.state @@ -338,8 +338,8 @@ class SessionTest { tracker.foregroundTimeout = 20 tracker.backgroundTimeout = 20 } - val tracker1 = Tracker(emitter, "tracker1", "app", null, context, trackerBuilder) - val tracker2 = Tracker(emitter, "tracker2", "app", null, context, trackerBuilder) + val tracker1 = Tracker(emitter, "tracker1", "app", context = context, builder = trackerBuilder) + val tracker2 = Tracker(emitter, "tracker2", "app", context = context, builder = trackerBuilder) val session1 = tracker1.session val session2 = tracker2.session session1!!.getSessionContext("session1-fake-id1", timestamp, false) @@ -364,7 +364,7 @@ class SessionTest { val id2 = session2.state!!.sessionId // Recreate tracker2 - val tracker2b = Tracker(emitter, "tracker2", "app", null, context, trackerBuilder) + val tracker2b = Tracker(emitter, "tracker2", "app", context = context, builder = trackerBuilder) tracker2b.session!!.getSessionContext("session2b-fake-id3", timestamp, false) val initialValue2b = tracker2b.session!!.sessionIndex?.toLong() val previousId2b = tracker2b.session!!.state!!.previousSessionId 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 3412e1d92..a3db9c82b 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 @@ -222,8 +222,8 @@ class EventSendingTest { ns, "myAppId", null, - InstrumentationRegistry.getInstrumentation().targetContext, - trackerBuilder + context = InstrumentationRegistry.getInstrumentation().targetContext, + builder = trackerBuilder ) emitter.eventStore.removeAllEvents() return tracker!! 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 4040450ae..fa8396383 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 @@ -21,6 +21,7 @@ import com.snowplowanalytics.core.utils.Util.addToMap import com.snowplowanalytics.core.utils.Util.mapHasKeys import com.snowplowanalytics.snowplow.configuration.PlatformContextProperty import com.snowplowanalytics.snowplow.payload.SelfDescribingJson +import com.snowplowanalytics.snowplow.tracker.PlatformContextRetriever /** * PlatformContext manages device information that is sent as context along with events. @@ -29,13 +30,16 @@ import com.snowplowanalytics.snowplow.payload.SelfDescribingJson * @param platformDictUpdateFrequency Minimal gap between subsequent updates of mobile platform information in milliseconds * @param networkDictUpdateFrequency Minimal gap between subsequent updates of network platform information in milliseconds * @param deviceInfoMonitor Device monitor for fetching platform information + * @param properties List of properties of the platform context to track + * @param retriever Overrides for retrieving property values */ class PlatformContext( - private val platformDictUpdateFrequency: Long, - private val networkDictUpdateFrequency: Long, - private val deviceInfoMonitor: DeviceInfoMonitor, - private val platformContextProperties: List?, - private val context: Context + private val platformDictUpdateFrequency: Long = 1000, + private val networkDictUpdateFrequency: Long = (10 * 1000).toLong(), + private val deviceInfoMonitor: DeviceInfoMonitor = DeviceInfoMonitor(), + private val properties: List? = null, + private val retriever: PlatformContextRetriever = PlatformContextRetriever(), + private val context: Context, ) { private val pairs: MutableMap = HashMap() private var lastUpdatedEphemeralPlatformDict: Long = 0 @@ -45,14 +49,6 @@ class PlatformContext( setPlatformDict() } - /** - * Initializes PlatformContext with default update intervals – 1s for updating platform information and 10s for updating network information. - * - * @param context the Android context - */ - constructor(platformContextProperties: List?, - context: Context) : this(1000, (10 * 1000).toLong(), DeviceInfoMonitor(), platformContextProperties, context) - fun getMobileContext(userAnonymisation: Boolean): SelfDescribingJson? { updateEphemeralDictsIfNecessary() @@ -90,41 +86,40 @@ class PlatformContext( } private fun setPlatformDict() { - addToMap(Parameters.OS_TYPE, deviceInfoMonitor.osType, pairs) - addToMap(Parameters.OS_VERSION, deviceInfoMonitor.osVersion, pairs) - addToMap(Parameters.DEVICE_MODEL, deviceInfoMonitor.deviceModel, pairs) - addToMap(Parameters.DEVICE_MANUFACTURER, deviceInfoMonitor.deviceVendor, pairs) + addToMap(Parameters.OS_TYPE, fromRetrieverOr(retriever.osType) { deviceInfoMonitor.osType }, pairs) + addToMap(Parameters.OS_VERSION, fromRetrieverOr(retriever.osVersion) { deviceInfoMonitor.osVersion }, pairs) + addToMap(Parameters.DEVICE_MODEL, fromRetrieverOr(retriever.deviceModel) { deviceInfoMonitor.deviceModel }, pairs) + addToMap(Parameters.DEVICE_MANUFACTURER, fromRetrieverOr(retriever.deviceVendor) { deviceInfoMonitor.deviceVendor }, pairs) // Carrier if (shouldTrack(PlatformContextProperty.CARRIER)) { addToMap( - Parameters.CARRIER, deviceInfoMonitor.getCarrier( - context - ), pairs + Parameters.CARRIER, fromRetrieverOr(retriever.carrier) { deviceInfoMonitor.getCarrier(context) }, + pairs ) } // Physical memory if (shouldTrack(PlatformContextProperty.PHYSICAL_MEMORY)) { addToMap( - Parameters.PHYSICAL_MEMORY, deviceInfoMonitor.getPhysicalMemory( - context - ), pairs + Parameters.PHYSICAL_MEMORY, + fromRetrieverOr(retriever.physicalMemory) { deviceInfoMonitor.getPhysicalMemory(context) }, + pairs ) } // Total storage if (shouldTrack(PlatformContextProperty.TOTAL_STORAGE)) { - addToMap(Parameters.TOTAL_STORAGE, deviceInfoMonitor.totalStorage, pairs) + addToMap(Parameters.TOTAL_STORAGE, fromRetrieverOr(retriever.totalStorage) { deviceInfoMonitor.totalStorage }, pairs) } // Resolution if (shouldTrack(PlatformContextProperty.RESOLUTION)) { - addToMap(Parameters.MOBILE_RESOLUTION, deviceInfoMonitor.getResolution(context), pairs) + addToMap(Parameters.MOBILE_RESOLUTION, fromRetrieverOr(retriever.resolution) { deviceInfoMonitor.getResolution(context) }, pairs) } // Scale if (shouldTrack(PlatformContextProperty.SCALE)) { - addToMap(Parameters.MOBILE_SCALE, deviceInfoMonitor.getScale(context), pairs) + addToMap(Parameters.MOBILE_SCALE, fromRetrieverOr(retriever.scale) { deviceInfoMonitor.getScale(context) }, pairs) } // Language if (shouldTrack(PlatformContextProperty.LANGUAGE)) { - addToMap(Parameters.MOBILE_LANGUAGE, deviceInfoMonitor.language?.take(8), pairs) + addToMap(Parameters.MOBILE_LANGUAGE, (fromRetrieverOr(retriever.language) { deviceInfoMonitor.language })?.take(8), pairs) } setEphemeralPlatformDict() @@ -140,9 +135,9 @@ class PlatformContext( val currentIdfa = pairs[Parameters.ANDROID_IDFA] if (currentIdfa == null || currentIdfa.toString().isEmpty()) { addToMap( - Parameters.ANDROID_IDFA, deviceInfoMonitor.getAndroidIdfa( - context - ), pairs + Parameters.ANDROID_IDFA, + fromRetrieverOr(retriever.androidIdfa) { deviceInfoMonitor.getAndroidIdfa(context) }, + pairs ) } } @@ -150,29 +145,25 @@ class PlatformContext( val trackBatState = shouldTrack(PlatformContextProperty.BATTERY_STATE) val trackBatLevel = shouldTrack(PlatformContextProperty.BATTERY_LEVEL) if (trackBatState || trackBatLevel) { - val batteryInfo = deviceInfoMonitor.getBatteryStateAndLevel( - context - ) - if (batteryInfo != null) { - if (trackBatState) { addToMap(Parameters.BATTERY_STATE, batteryInfo.first, pairs) } - if (trackBatLevel) { addToMap(Parameters.BATTERY_LEVEL, batteryInfo.second, pairs) } - } + val batteryInfo = deviceInfoMonitor.getBatteryStateAndLevel(context) + if (trackBatState) { addToMap(Parameters.BATTERY_STATE, fromRetrieverOr(retriever.batteryState) { batteryInfo?.first }, pairs) } + if (trackBatLevel) { addToMap(Parameters.BATTERY_LEVEL, fromRetrieverOr(retriever.batteryLevel) { batteryInfo?.second }, pairs) } } // Memory if (shouldTrack(PlatformContextProperty.SYSTEM_AVAILABLE_MEMORY)) { addToMap( - Parameters.SYSTEM_AVAILABLE_MEMORY, deviceInfoMonitor.getSystemAvailableMemory( - context - ), pairs + Parameters.SYSTEM_AVAILABLE_MEMORY, + fromRetrieverOr(retriever.systemAvailableMemory) { deviceInfoMonitor.getSystemAvailableMemory(context) }, + pairs ) } // Storage if (shouldTrack(PlatformContextProperty.AVAILABLE_STORAGE)) { - addToMap(Parameters.AVAILABLE_STORAGE, deviceInfoMonitor.availableStorage, pairs) + addToMap(Parameters.AVAILABLE_STORAGE, fromRetrieverOr(retriever.availableStorage) { deviceInfoMonitor.availableStorage }, pairs) } // Is portrait if (shouldTrack(PlatformContextProperty.IS_PORTRAIT)) { - addToMap(Parameters.IS_PORTRAIT, deviceInfoMonitor.getIsPortrait(context), pairs) + addToMap(Parameters.IS_PORTRAIT, fromRetrieverOr(retriever.isPortrait) { deviceInfoMonitor.getIsPortrait(context) }, pairs) } } @@ -187,14 +178,14 @@ class PlatformContext( if (trackType) { addToMap( Parameters.NETWORK_TYPE, - deviceInfoMonitor.getNetworkType(networkInfo), + fromRetrieverOr(retriever.networkType) { deviceInfoMonitor.getNetworkType(networkInfo) }, pairs ) } if (trackTech) { addToMap( Parameters.NETWORK_TECHNOLOGY, - deviceInfoMonitor.getNetworkTechnology(networkInfo), + fromRetrieverOr(retriever.networkTechnology) { deviceInfoMonitor.getNetworkTechnology(networkInfo) }, pairs ) } @@ -215,29 +206,45 @@ class PlatformContext( TrackerConstants.SNOWPLOW_GENERAL_VARS, Context.MODE_PRIVATE ) - val appSetId = generalPref.getString(Parameters.APP_SET_ID, null) - val appSetIdScope = generalPref.getString(Parameters.APP_SET_ID_SCOPE, null) + 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) } } else { Executor.execute(TAG) { - deviceInfoMonitor.getAppSetIdAndScope(context)?.let { - if (trackId) { addToMap(Parameters.APP_SET_ID, it.first, pairs) } - if (trackScope) { addToMap(Parameters.APP_SET_ID_SCOPE, it.second, pairs) } - - generalPref.edit() - .putString(Parameters.APP_SET_ID, it.first) - .putString(Parameters.APP_SET_ID_SCOPE, it.second) - .apply() + 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() } } } } private fun shouldTrack(property: PlatformContextProperty): Boolean { - return platformContextProperties?.contains(property) ?: true + return properties?.contains(property) ?: true + } + + private fun fromRetrieverOr(f1: (() -> T)?, f2: () -> T): T { + return if (f1 == null) { f2() } else { f1.invoke() } } companion object { 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 1822196d5..059113282 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 @@ -302,12 +302,13 @@ class ServiceProvider( } val tracker = Tracker( - emitter, - namespace, - trackerConfiguration.appId, - trackerConfiguration.platformContextProperties, - context, - builder + emitter = emitter, + namespace = namespace, + appId = trackerConfiguration.appId, + platformContextProperties = trackerConfiguration.platformContextProperties, + platformContextRetriever = trackerConfiguration.platformContextRetriever, + context = context, + builder = builder ) if (trackerConfiguration.isPaused) { 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 668f5c68b..86760fc8e 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 @@ -45,7 +45,6 @@ import com.snowplowanalytics.snowplow.util.Basis import java.util.* import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicBoolean -import kotlin.math.max /** * Builds a Tracker object which is used to send events to a Snowplow Collector. @@ -60,7 +59,8 @@ class Tracker( emitter: Emitter, val namespace: String, var appId: String, - platformContextProperties: List?, + platformContextProperties: List? = null, + platformContextRetriever: PlatformContextRetriever? = null, context: Context, builder: ((Tracker) -> Unit)? = null) { private var builderFinished = false @@ -88,7 +88,11 @@ class Tracker( val dataCollection: Boolean get() = _dataCollection.get() - private val platformContextManager = PlatformContext(platformContextProperties, context) + private val platformContextManager = PlatformContext( + properties = platformContextProperties, + retriever = platformContextRetriever ?: PlatformContextRetriever(), + context = context + ) var emitter: Emitter = emitter set(emitter) { 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 47a5872bc..f8f78cbe3 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 @@ -13,12 +13,12 @@ package com.snowplowanalytics.snowplow.configuration import com.snowplowanalytics.core.tracker.Logger -import com.snowplowanalytics.core.tracker.PlatformContext import com.snowplowanalytics.core.tracker.TrackerConfigurationInterface import com.snowplowanalytics.core.tracker.TrackerDefaults import com.snowplowanalytics.snowplow.tracker.DevicePlatform import com.snowplowanalytics.snowplow.tracker.LogLevel import com.snowplowanalytics.snowplow.tracker.LoggerDelegate +import com.snowplowanalytics.snowplow.tracker.PlatformContextRetriever import org.json.JSONObject import java.util.* @@ -166,6 +166,15 @@ open class TrackerConfiguration : TrackerConfigurationInterface, Configuration { get() = _platformContextProperties ?: sourceConfig?.platformContextProperties set(value) { _platformContextProperties = value } + private var _platformContextRetriever: PlatformContextRetriever? = null + /** + * Set of callbacks to be used to retrieve properties of the platform context. + * Overrides the tracker implementation for setting the properties. + */ + open var platformContextRetriever: PlatformContextRetriever? + get() = _platformContextRetriever ?: sourceConfig?.platformContextRetriever + set(value) { _platformContextRetriever = value } + // Builder methods /** @@ -345,6 +354,15 @@ open class TrackerConfiguration : TrackerConfigurationInterface, Configuration { return this } + /** + * Set of callbacks to be used to retrieve properties of the platform context. + * Overrides the tracker implementation for setting the properties. + */ + fun platformContextRetriever(platformContextRetriever: PlatformContextRetriever?): TrackerConfiguration { + this.platformContextRetriever = platformContextRetriever + return this + } + // Copyable override fun copy(): Configuration { return TrackerConfiguration(appId) @@ -367,6 +385,7 @@ open class TrackerConfiguration : TrackerConfigurationInterface, Configuration { .userAnonymisation(userAnonymisation) .trackerVersionSuffix(trackerVersionSuffix) .platformContextProperties(platformContextProperties) + .platformContextRetriever(platformContextRetriever) } /** diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/PlatformContextRetriever.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/PlatformContextRetriever.kt new file mode 100644 index 000000000..1787a2865 --- /dev/null +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/PlatformContextRetriever.kt @@ -0,0 +1,78 @@ +/* + * 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.tracker + +/** + * Overrides for the values for properties of the platform context. + */ +data class PlatformContextRetriever( + /// Operating system type (e.g., ios, tvos, watchos, osx, android) + var osType: (() -> String)? = null, + + /// The current version of the operating system + var osVersion: (() -> String)? = null, + + /// The manufacturer of the product/hardware + var deviceVendor: (() -> String)? = null, + + /// The end-user-visible name for the end product + var deviceModel: (() -> String)? = null, + + /// The carrier of the SIM inserted in the device + var carrier: (() -> String?)? = null, + + /// Type of network the device is connected to + var networkType: (() -> String?)? = null, + + /// Radio access technology that the device is using + var networkTechnology: (() -> String?)? = null, + + /// Advertising identifier on Android + var androidIdfa: (() -> String?)? = null, + + /// Bytes of storage remaining + var availableStorage: (() -> Long?)? = null, + + /// Total size of storage in bytes + var totalStorage: (() -> Long?)? = null, + + /// Total physical system memory in bytes + var physicalMemory: (() -> Long?)? = null, + + /// Available memory on the system in bytes (Android only) + var systemAvailableMemory: (() -> Long?)? = null, + + /// Remaining battery level as an integer percentage of total battery capacity + var batteryLevel: (() -> Int?)? = null, + + /// Battery state for the device + var batteryState: (() -> String?)? = null, + + /// A Boolean indicating whether the device orientation is portrait (either upright or upside down) + var isPortrait: (() -> Boolean?)? = null, + + /// Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes + var resolution: (() -> String?)? = null, + + /// Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS) + var scale: (() -> Float?)? = null, + + /// System language currently used on the device (ISO 639) + var language: (() -> String)? = null, + + /// Android vendor ID scoped to the set of apps published under the same Google Play developer account (see https://developer.android.com/training/articles/app-set-id) + var appSetId: (() -> String)? = null, + + /// Scope of the `appSetId`. Can be scoped to the app or to a developer account on an app store (all apps from the same developer on the same device will have the same ID) + var appSetIdScope: (() -> String)? = null +) From 2594734d0e347ce654d0efabef63094b19287140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Tue, 30 Jan 2024 10:12:56 +0100 Subject: [PATCH 2/2] Address PR comments --- .../core/tracker/PlatformContext.kt | 1 + .../snowplowanalytics/core/tracker/Tracker.kt | 2 + .../tracker/PlatformContextRetriever.kt | 82 ++++++++++++++----- 3 files changed, 64 insertions(+), 21 deletions(-) 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 fa8396383..31de79bda 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 @@ -32,6 +32,7 @@ import com.snowplowanalytics.snowplow.tracker.PlatformContextRetriever * @param deviceInfoMonitor Device monitor for fetching platform information * @param properties List of properties of the platform context to track * @param retriever Overrides for retrieving property values + * @param context Android context */ class PlatformContext( private val platformDictUpdateFrequency: Long = 1000, 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 86760fc8e..779e38743 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 @@ -52,6 +52,8 @@ import java.util.concurrent.atomic.AtomicBoolean * @param emitter Emitter to which events will be sent * @param namespace Identifier for the Tracker instance * @param appId Application ID + * @param platformContextProperties List of properties of the platform context to track + * @param platformContextRetriever Overrides for retrieving property values * @param context The Android application context * @param builder A closure to set Tracker configuration */ diff --git a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/PlatformContextRetriever.kt b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/PlatformContextRetriever.kt index 1787a2865..aec635395 100644 --- a/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/PlatformContextRetriever.kt +++ b/snowplow-tracker/src/main/java/com/snowplowanalytics/snowplow/tracker/PlatformContextRetriever.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2023 Snowplow Analytics Ltd. All rights reserved. + * Copyright (c) 2015-present 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. @@ -16,63 +16,103 @@ package com.snowplowanalytics.snowplow.tracker * Overrides for the values for properties of the platform context. */ data class PlatformContextRetriever( - /// Operating system type (e.g., ios, tvos, watchos, osx, android) + /** + * Operating system type (e.g., ios, tvos, watchos, osx, android). + */ var osType: (() -> String)? = null, - /// The current version of the operating system + /* + * The current version of the operating system. + */ var osVersion: (() -> String)? = null, - /// The manufacturer of the product/hardware + /** + * The manufacturer of the product/hardware. + */ var deviceVendor: (() -> String)? = null, - /// The end-user-visible name for the end product + /** + * The end-user-visible name for the end product. + */ var deviceModel: (() -> String)? = null, - /// The carrier of the SIM inserted in the device + /** + * The carrier of the SIM inserted in the device. + */ var carrier: (() -> String?)? = null, - /// Type of network the device is connected to + /** + * Type of network the device is connected to. + */ var networkType: (() -> String?)? = null, - /// Radio access technology that the device is using + /** + * Radio access technology that the device is using. + */ var networkTechnology: (() -> String?)? = null, - /// Advertising identifier on Android + /** + * Advertising identifier on Android. + */ var androidIdfa: (() -> String?)? = null, - /// Bytes of storage remaining + /** + * Bytes of storage remaining. + */ var availableStorage: (() -> Long?)? = null, - /// Total size of storage in bytes + /** + * Total size of storage in bytes. + */ var totalStorage: (() -> Long?)? = null, - /// Total physical system memory in bytes + /** + * Total physical system memory in bytes. + */ var physicalMemory: (() -> Long?)? = null, - /// Available memory on the system in bytes (Android only) + /** + * Available memory on the system in bytes (Android only). + */ var systemAvailableMemory: (() -> Long?)? = null, - /// Remaining battery level as an integer percentage of total battery capacity + /** + * Remaining battery level as an integer percentage of total battery capacity. + */ var batteryLevel: (() -> Int?)? = null, - /// Battery state for the device + /** + * Battery state for the device + */ var batteryState: (() -> String?)? = null, - /// A Boolean indicating whether the device orientation is portrait (either upright or upside down) + /** + * A Boolean indicating whether the device orientation is portrait (either upright or upside down). + */ var isPortrait: (() -> Boolean?)? = null, - /// Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes + /** + * Screen resolution in pixels. Arrives in the form of WIDTHxHEIGHT (e.g., 1200x900). Doesn't change when device orientation changes. + */ var resolution: (() -> String?)? = null, - /// Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS) + /** + * Scale factor used to convert logical coordinates to device coordinates of the screen (uses UIScreen.scale on iOS). + */ var scale: (() -> Float?)? = null, - /// System language currently used on the device (ISO 639) + /** + * System language currently used on the device (ISO 639). + */ var language: (() -> String)? = null, - /// Android vendor ID scoped to the set of apps published under the same Google Play developer account (see https://developer.android.com/training/articles/app-set-id) + /** + * Android vendor ID scoped to the set of apps published under the same Google Play developer account (see https://developer.android.com/training/articles/app-set-id). + */ var appSetId: (() -> String)? = null, - /// Scope of the `appSetId`. Can be scoped to the app or to a developer account on an app store (all apps from the same developer on the same device will have the same ID) + /** + * Scope of the `appSetId`. Can be scoped to the app or to a developer account on an app store (all apps from the same developer on the same device will have the same ID). + */ var appSetIdScope: (() -> String)? = null )