From f5beab516339df5c298dbe4ab615d58a136fc3cd Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Thu, 19 Oct 2023 13:04:47 +0400 Subject: [PATCH 1/6] Split events into chunks before sending --- CHANGELOG.md | 5 +++++ gradle/libs.versions.toml | 8 ++++---- piano-analytics/gradle.properties | 2 +- .../src/main/java/io/piano/android/analytics/SendTask.kt | 5 ++++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6febdcb..ae33ef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Piano Analytics SDK for Android +## v3.3.4-SNAPSHOT +* Updated dependencies: + - com.squareup.okhttp3:okhttp [4.11.0 -> 4.12.0] + https://square.github.io/okhttp/ + ## v3.3.3 * Added `track` function to `MediaHelper` for tracking custom media events. * Changed behavior: `extraProps` in `MediaHelper` are added to all events, not only to "heartbeat" events. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8fcdaaa..ef5819b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] # Plugins kotlin = "1.8.22" -android = "8.1.1" -versionUpdater = "0.47.0" -ktlint = "11.5.1" +android = "8.1.2" +versionUpdater = "0.49.0" +ktlint = "11.6.1" dokka = "1.8.20" mavenRelease = "0.25.3" moshiIR = "0.22.1" @@ -18,7 +18,7 @@ materialLibrary = "1.9.0" googleAdsId = "18.0.1" huaweiAdsId = "3.4.26.303" retrofit = "2.6.4" -okhttp = "4.11.0" +okhttp = "4.12.0" moshi = "1.15.0" timber = "5.0.1" viewBindingProperty = "1.5.9" diff --git a/piano-analytics/gradle.properties b/piano-analytics/gradle.properties index a123ac4..5f574cd 100644 --- a/piano-analytics/gradle.properties +++ b/piano-analytics/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=3.3.3 +VERSION_NAME=3.3.4-SNAPSHOT GROUP=io.piano.android POM_NAME=Analytics POM_ARTIFACT_ID=analytics diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/SendTask.kt b/piano-analytics/src/main/java/io/piano/android/analytics/SendTask.kt index 2d9a046..8a1271e 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/SendTask.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/SendTask.kt @@ -28,7 +28,9 @@ internal class SendTask( Timber.w("Can't send events - no connection") return } - send(eventRepository.getNotSentEvents()) + eventRepository.getNotSentEvents().chunked(CHUNK_SIZE).forEach { + send(it) + } } internal fun send(events: List) { @@ -58,5 +60,6 @@ internal class SendTask( companion object { internal val MEDIA_TYPE by lazy(LazyThreadSafetyMode.NONE) { "application/json; charset=UTF-8".toMediaType() } + private const val CHUNK_SIZE = 50 } } From dd5e1236ed3e626ff27926b434e8d85dfe38cf51 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Mon, 23 Oct 2023 18:52:51 +0400 Subject: [PATCH 2/6] Add consumer rules #12 --- piano-analytics/consumer-rules.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/piano-analytics/consumer-rules.pro b/piano-analytics/consumer-rules.pro index e69de29..4190ee6 100644 --- a/piano-analytics/consumer-rules.pro +++ b/piano-analytics/consumer-rules.pro @@ -0,0 +1,2 @@ +-dontwarn com.google.android.gms.** +-dontwarn com.huawei.hms.ads.** From 756b0b1f866f21d6884a03f0e7d5a1af67021e33 Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Mon, 23 Oct 2023 19:01:20 +0400 Subject: [PATCH 3/6] Update to new ktlint --- .editorconfig | 6 +- piano-analytics/build.gradle.kts | 8 +- .../piano/android/analytics/DatabaseHelper.kt | 110 +++++++-------- .../io/piano/android/analytics/Delegates.kt | 33 +---- .../android/analytics/EventRepository.kt | 9 +- .../io/piano/android/analytics/MediaHelper.kt | 128 ++++++++---------- .../piano/android/analytics/PianoAnalytics.kt | 13 +- .../piano/android/analytics/PrefsStorage.kt | 17 ++- .../java/io/piano/android/analytics/Utils.kt | 15 +- .../eventprocessors/GroupEventProcessor.kt | 7 +- .../eventprocessors/PrivacyEventProcessor.kt | 11 +- .../eventprocessors/UserEventProcessor.kt | 25 ++-- .../analytics/idproviders/UuidIdProvider.kt | 9 +- 13 files changed, 175 insertions(+), 216 deletions(-) diff --git a/.editorconfig b/.editorconfig index d6e7126..225962c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -244,7 +244,7 @@ ij_editorconfig_space_before_colon = false ij_editorconfig_space_before_comma = false ij_editorconfig_spaces_around_assignment_operators = true -[{*.gradle, *.groovy, *.gant, *.gdsl, *.gy}] +[{*.gradle,*.groovy,*.gant,*.gdsl,*.gy}] ij_groovy_align_group_field_declarations = false ij_groovy_align_multiline_array_initializer_expression = false ij_groovy_align_multiline_assignment = false @@ -398,7 +398,8 @@ ij_groovy_while_brace_force = never ij_groovy_while_on_new_line = false ij_groovy_wrap_long_lines = false -[*.{kt, kts}] +[*.{kt,kts}] +ktlint_code_style = android_studio ktlint_standard_import-ordering = disabled ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_multiline_binary_operation = false @@ -406,6 +407,7 @@ ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = false ij_kotlin_assignment_wrap = normal ij_kotlin_blank_lines_after_class_header = 0 diff --git a/piano-analytics/build.gradle.kts b/piano-analytics/build.gradle.kts index 21d2456..f18dd9f 100644 --- a/piano-analytics/build.gradle.kts +++ b/piano-analytics/build.gradle.kts @@ -6,8 +6,12 @@ plugins { alias(libs.plugins.mavenRelease) } +@Suppress("PropertyName") val GROUP: String by project + +@Suppress("PropertyName") val VERSION_NAME: String by project + group = GROUP version = VERSION_NAME @@ -43,8 +47,8 @@ kotlin { } ktlint { - version.set("0.50.0") - android.set(true) + android = true + version = "1.0.1" } dependencies { diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/DatabaseHelper.kt b/piano-analytics/src/main/java/io/piano/android/analytics/DatabaseHelper.kt index bcc4743..6ce232a 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/DatabaseHelper.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/DatabaseHelper.kt @@ -42,39 +42,35 @@ internal class DatabaseHelper( } } - internal fun EventRecord.toContentValues(): ContentValues = - ContentValues().apply { - put(EventRecord.DATA, dataEncoder.encode(data)) - put(EventRecord.TIME, timestamp) - put(EventRecord.IS_SENT, isSent) - } + internal fun EventRecord.toContentValues(): ContentValues = ContentValues().apply { + put(EventRecord.DATA, dataEncoder.encode(data)) + put(EventRecord.TIME, timestamp) + put(EventRecord.IS_SENT, isSent) + } - fun ContentValues.toEventRecord(): EventRecord = - EventRecord( - dataEncoder.decode(getAsString(EventRecord.DATA)), - getAsLong(EventRecord.TIME), - getAsLong(EventRecord.ID), - getAsBoolean(EventRecord.IS_SENT) - ) + fun ContentValues.toEventRecord(): EventRecord = EventRecord( + dataEncoder.decode(getAsString(EventRecord.DATA)), + getAsLong(EventRecord.TIME), + getAsLong(EventRecord.ID), + getAsBoolean(EventRecord.IS_SENT) + ) - fun save(eventRecord: EventRecord): Long = - eventRecord.id?.let { id -> - writableDatabase.update( - EventRecord.TABLE_NAME, - eventRecord.toContentValues(), - "${EventRecord.ID} = ?", - arrayOf(id.toString()) - ).toLong() - } ?: writableDatabase.insert( + fun save(eventRecord: EventRecord): Long = eventRecord.id?.let { id -> + writableDatabase.update( EventRecord.TABLE_NAME, - null, - eventRecord.toContentValues() - ) + eventRecord.toContentValues(), + "${EventRecord.ID} = ?", + arrayOf(id.toString()) + ).toLong() + } ?: writableDatabase.insert( + EventRecord.TABLE_NAME, + null, + eventRecord.toContentValues() + ) - fun delete(eventRecord: EventRecord): Int = - eventRecord.id?.let { id -> - delete("${EventRecord.ID} = ?", id.toString()) - } ?: -1 + fun delete(eventRecord: EventRecord): Int = eventRecord.id?.let { id -> + delete("${EventRecord.ID} = ?", id.toString()) + } ?: -1 fun delete(whereClause: String?, vararg whereArgs: String): Int = writableDatabase.delete(EventRecord.TABLE_NAME, whereClause, whereArgs) @@ -87,42 +83,40 @@ internal class DatabaseHelper( having: String? = null, orderBy: String? = "${EventRecord.TIME} ASC", limit: String? = null, - ): List = - readableDatabase.query( - EventRecord.TABLE_NAME, - columns, - selection, - selectionArgs, - groupBy, - having, - orderBy, - limit - ).use { c -> - generateSequence { if (c.moveToNext()) c else null } - .map(Companion::cursorRowToContentValues) - .map { - it.toEventRecord() - }.filter { - it.isValid - }.toList() - } + ): List = readableDatabase.query( + EventRecord.TABLE_NAME, + columns, + selection, + selectionArgs, + groupBy, + having, + orderBy, + limit + ).use { c -> + generateSequence { if (c.moveToNext()) c else null } + .map(Companion::cursorRowToContentValues) + .map { + it.toEventRecord() + }.filter { + it.isValid + }.toList() + } companion object { const val DATABASE_VERSION = 1 private const val DATABASE_NAME = "events.db" @JvmStatic - private fun cursorRowToContentValues(c: Cursor): ContentValues = - ContentValues().apply { - for (i in 0 until c.columnCount) { - val name = c.columnNames[i] - when (c.getType(i)) { - Cursor.FIELD_TYPE_BLOB -> put(name, c.getBlob(i)) - Cursor.FIELD_TYPE_FLOAT -> put(name, c.getFloat(i)) - Cursor.FIELD_TYPE_INTEGER -> put(name, c.getLong(i)) - else -> put(name, c.getString(i)) - } + private fun cursorRowToContentValues(c: Cursor): ContentValues = ContentValues().apply { + for (i in 0 until c.columnCount) { + val name = c.columnNames[i] + when (c.getType(i)) { + Cursor.FIELD_TYPE_BLOB -> put(name, c.getBlob(i)) + Cursor.FIELD_TYPE_FLOAT -> put(name, c.getFloat(i)) + Cursor.FIELD_TYPE_INTEGER -> put(name, c.getLong(i)) + else -> put(name, c.getString(i)) } } + } } } diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/Delegates.kt b/piano-analytics/src/main/java/io/piano/android/analytics/Delegates.kt index 35dbcc0..b050ca4 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/Delegates.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/Delegates.kt @@ -19,10 +19,7 @@ internal inline fun delegatedPropertyWithDefaultValue( } } -internal fun resettableProperty( - resetValue: T, - initializer: () -> T, -) = ResettableProperty(resetValue, initializer) +internal fun resettableProperty(resetValue: T, initializer: () -> T) = ResettableProperty(resetValue, initializer) internal class ResettableProperty( private val resetValue: T, @@ -48,29 +45,25 @@ internal class SharedPreferenceDelegates(private val prefs: SharedPreferences) { default: Boolean = false, key: String? = null, canBeSaved: (key: String) -> Boolean = DEFAULT_SAVE_FILTER, - ): ReadWriteProperty = - create(default, key, canBeSaved, prefs::getBoolean, prefs.edit()::putBoolean) + ): ReadWriteProperty = create(default, key, canBeSaved, prefs::getBoolean, prefs.edit()::putBoolean) fun int( default: Int = 0, key: String? = null, canBeSaved: (key: String) -> Boolean = DEFAULT_SAVE_FILTER, - ): ReadWriteProperty = - create(default, key, canBeSaved, prefs::getInt, prefs.edit()::putInt) + ): ReadWriteProperty = create(default, key, canBeSaved, prefs::getInt, prefs.edit()::putInt) fun float( default: Float = 0f, key: String? = null, canBeSaved: (key: String) -> Boolean = DEFAULT_SAVE_FILTER, - ): ReadWriteProperty = - create(default, key, canBeSaved, prefs::getFloat, prefs.edit()::putFloat) + ): ReadWriteProperty = create(default, key, canBeSaved, prefs::getFloat, prefs.edit()::putFloat) fun long( default: Long = 0L, key: String? = null, canBeSaved: (key: String) -> Boolean = DEFAULT_SAVE_FILTER, - ): ReadWriteProperty = - create(default, key, canBeSaved, prefs::getLong, prefs.edit()::putLong) + ): ReadWriteProperty = create(default, key, canBeSaved, prefs::getLong, prefs.edit()::putLong) fun string( default: String = "", @@ -86,19 +79,6 @@ internal class SharedPreferenceDelegates(private val prefs: SharedPreferences) { ): ReadWriteProperty = create(default, key, canBeSaved, { k, d -> prefs.getString(k, d) }, prefs.edit()::putString) - fun stringSet( - default: Set = emptySet(), - key: String? = null, - canBeSaved: (key: String) -> Boolean = DEFAULT_SAVE_FILTER, - ): ReadWriteProperty> = - create( - default, - key, - canBeSaved, - { k, d -> prefs.getStringSet(k, d) as Set }, - prefs.edit()::putStringSet - ) - private fun create( default: T, key: String? = null, @@ -108,8 +88,7 @@ internal class SharedPreferenceDelegates(private val prefs: SharedPreferences) { ) = object : ReadWriteProperty { private fun key(property: KProperty<*>) = key ?: property.name - override fun getValue(thisRef: Any, property: KProperty<*>): T = - getter(key(property), default) + override fun getValue(thisRef: Any, property: KProperty<*>): T = getter(key(property), default) override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { val propertyKey = key(property) diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/EventRepository.kt b/piano-analytics/src/main/java/io/piano/android/analytics/EventRepository.kt index c0b850f..d7aeda4 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/EventRepository.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/EventRepository.kt @@ -22,11 +22,10 @@ internal class EventRepository( ) } - fun getNotSentEvents(): List = - databaseHelper.query( - selection = "${EventRecord.IS_SENT} = 0", - orderBy = "${EventRecord.TIME} ASC" - ) + fun getNotSentEvents(): List = databaseHelper.query( + selection = "${EventRecord.IS_SENT} = 0", + orderBy = "${EventRecord.TIME} ASC" + ) fun markEventsAsSent(events: Collection) { events.forEach { e -> diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/MediaHelper.kt b/piano-analytics/src/main/java/io/piano/android/analytics/MediaHelper.kt index b590f49..1f256ce 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/MediaHelper.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/MediaHelper.kt @@ -141,8 +141,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun bufferHeartbeat(vararg properties: Property) = - processBufferHeartbeat(false, *properties) + fun bufferHeartbeat(vararg properties: Property) = processBufferHeartbeat(false, *properties) /** * Generate heartbeat during rebuffering. @@ -150,8 +149,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun rebufferHeartbeat(vararg properties: Property) = - processRebufferHeartbeat(false, *properties) + fun rebufferHeartbeat(vararg properties: Property) = processRebufferHeartbeat(false, *properties) /** * Generate play event (play attempt). @@ -210,25 +208,24 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun playbackStart(cursorPosition: Int, vararg properties: Property) = - processEvent(AV_START, properties) { - previousCursorPositionMillis = cursorPosition.coerceAtLeast(0) - currentCursorPositionMillis = previousCursorPositionMillis - bufferTimeMillis = 0 - isPlaying = true - isPlaybackActivated = true + fun playbackStart(cursorPosition: Int, vararg properties: Property) = processEvent(AV_START, properties) { + previousCursorPositionMillis = cursorPosition.coerceAtLeast(0) + currentCursorPositionMillis = previousCursorPositionMillis + bufferTimeMillis = 0 + isPlaying = true + isPlaybackActivated = true - restartHeartbeatExecutor() - if (autoHeartbeat) { - previousHeartbeatDelay = rescheduleRunnable( - previousHeartbeatDelay, - startSessionTimeMillis, - MIN_HEARTBEAT_DURATION, - heartbeatDurations, - heartbeatRunnable - ) - } + restartHeartbeatExecutor() + if (autoHeartbeat) { + previousHeartbeatDelay = rescheduleRunnable( + previousHeartbeatDelay, + startSessionTimeMillis, + MIN_HEARTBEAT_DURATION, + heartbeatDurations, + heartbeatRunnable + ) } + } /** * Media playback paused. @@ -237,15 +234,14 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun playbackPaused(cursorPosition: Int, vararg properties: Property) = - processEvent(AV_PAUSE, properties) { - previousCursorPositionMillis = currentCursorPositionMillis - currentCursorPositionMillis = cursorPosition.coerceAtLeast(0) - bufferTimeMillis = 0 - isPlaying = false - isPlaybackActivated = true - restartHeartbeatExecutor() - } + fun playbackPaused(cursorPosition: Int, vararg properties: Property) = processEvent(AV_PAUSE, properties) { + previousCursorPositionMillis = currentCursorPositionMillis + currentCursorPositionMillis = cursorPosition.coerceAtLeast(0) + bufferTimeMillis = 0 + isPlaying = false + isPlaybackActivated = true + restartHeartbeatExecutor() + } /** * Media playback restarted manually after a pause. @@ -254,25 +250,24 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun playbackResumed(cursorPosition: Int, vararg properties: Property) = - processEvent(AV_RESUME, properties) { - previousCursorPositionMillis = currentCursorPositionMillis - currentCursorPositionMillis = cursorPosition.coerceAtLeast(0) - bufferTimeMillis = 0 - isPlaying = true - isPlaybackActivated = true + fun playbackResumed(cursorPosition: Int, vararg properties: Property) = processEvent(AV_RESUME, properties) { + previousCursorPositionMillis = currentCursorPositionMillis + currentCursorPositionMillis = cursorPosition.coerceAtLeast(0) + bufferTimeMillis = 0 + isPlaying = true + isPlaybackActivated = true - restartHeartbeatExecutor() - if (autoHeartbeat) { - previousHeartbeatDelay = rescheduleRunnable( - previousHeartbeatDelay, - startSessionTimeMillis, - MIN_HEARTBEAT_DURATION, - heartbeatDurations, - heartbeatRunnable - ) - } + restartHeartbeatExecutor() + if (autoHeartbeat) { + previousHeartbeatDelay = rescheduleRunnable( + previousHeartbeatDelay, + startSessionTimeMillis, + MIN_HEARTBEAT_DURATION, + heartbeatDurations, + heartbeatRunnable + ) } + } /** * Media playback stopped. @@ -356,8 +351,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun adClick(vararg properties: Property) = - pianoAnalytics.sendEvents(buildEvent(AV_AD_CLICK, false, *properties)) + fun adClick(vararg properties: Property) = pianoAnalytics.sendEvents(buildEvent(AV_AD_CLICK, false, *properties)) /** * Measuring media skip (especially for ads). @@ -365,8 +359,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun adSkip(vararg properties: Property) = - pianoAnalytics.sendEvents(buildEvent(AV_AD_SKIP, false, *properties)) + fun adSkip(vararg properties: Property) = pianoAnalytics.sendEvents(buildEvent(AV_AD_SKIP, false, *properties)) /** * Measuring reco or Ad display. @@ -374,8 +367,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun display(vararg properties: Property) = - pianoAnalytics.sendEvents(buildEvent(AV_DISPLAY, false, *properties)) + fun display(vararg properties: Property) = pianoAnalytics.sendEvents(buildEvent(AV_DISPLAY, false, *properties)) /** * Measuring close action. @@ -383,8 +375,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun close(vararg properties: Property) = - pianoAnalytics.sendEvents(buildEvent(AV_CLOSE, false, *properties)) + fun close(vararg properties: Property) = pianoAnalytics.sendEvents(buildEvent(AV_CLOSE, false, *properties)) /** * Measurement of a volume change action. @@ -392,8 +383,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun volume(vararg properties: Property) = - pianoAnalytics.sendEvents(buildEvent(AV_VOLUME, false, *properties)) + fun volume(vararg properties: Property) = pianoAnalytics.sendEvents(buildEvent(AV_VOLUME, false, *properties)) /** * Measurement of activated subtitles. @@ -437,8 +427,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun quality(vararg properties: Property) = - pianoAnalytics.sendEvents(buildEvent(AV_QUALITY, false, *properties)) + fun quality(vararg properties: Property) = pianoAnalytics.sendEvents(buildEvent(AV_QUALITY, false, *properties)) /** * Measurement of a speed change action. @@ -446,8 +435,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun speed(vararg properties: Property) = - pianoAnalytics.sendEvents(buildEvent(AV_SPEED, false, *properties)) + fun speed(vararg properties: Property) = pianoAnalytics.sendEvents(buildEvent(AV_SPEED, false, *properties)) /** * Measurement of a sharing action. @@ -455,8 +443,7 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun share(vararg properties: Property) = - pianoAnalytics.sendEvents(buildEvent(AV_SHARE, false, *properties)) + fun share(vararg properties: Property) = pianoAnalytics.sendEvents(buildEvent(AV_SHARE, false, *properties)) /** * Error measurement preventing reading from continuing. @@ -465,15 +452,14 @@ class MediaHelper internal constructor( * @param properties extra properties for event */ @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. - fun error(message: String, vararg properties: Property) = - pianoAnalytics.sendEvents( - buildEvent( - AV_ERROR, - false, - Property(PropertyName("av_player_error"), message), - *properties - ) + fun error(message: String, vararg properties: Property) = pianoAnalytics.sendEvents( + buildEvent( + AV_ERROR, + false, + Property(PropertyName("av_player_error"), message), + *properties ) + ) /** * Track custom event, don't use it for built-in events. diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/PianoAnalytics.kt b/piano-analytics/src/main/java/io/piano/android/analytics/PianoAnalytics.kt index 2aa0ea5..cac6453 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/PianoAnalytics.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/PianoAnalytics.kt @@ -31,11 +31,14 @@ class PianoAnalytics internal constructor( private val visitorIdProvider: VisitorIdProvider, private val customIdProvider: CustomIdProvider, customEventProcessorsGroup: GroupEventProcessor, - @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. + // Public API. + @Suppress("unused", "MemberVisibilityCanBePrivate") val privacyModesStorage: PrivacyModesStorage, - @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. + // Public API. + @Suppress("unused", "MemberVisibilityCanBePrivate") val contextPropertiesStorage: ContextPropertiesStorage, - @Suppress("unused", "MemberVisibilityCanBePrivate") // Public API. + // Public API. + @Suppress("unused", "MemberVisibilityCanBePrivate") val userStorage: UserStorage, ) { private val executor: ScheduledExecutorService = executorProvider() @@ -116,9 +119,7 @@ class PianoAnalytics internal constructor( * @param events a custom event list */ @Suppress("unused") // Public API. - fun sendEvents( - vararg events: Event, - ) { + fun sendEvents(vararg events: Event) { // delay is required, see androidx.lifecycle.ProcessLifecycleOwner.TIMEOUT_MS executor.schedule( { diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/PrefsStorage.kt b/piano-analytics/src/main/java/io/piano/android/analytics/PrefsStorage.kt index c1ac752..d62dd49 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/PrefsStorage.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/PrefsStorage.kt @@ -17,16 +17,15 @@ internal class PrefsStorage( internal fun clear() = prefs.edit().clear().apply() - internal fun cleanStorageFeature(privacyStorageFeature: PrivacyStorageFeature) = - prefs.edit().apply { - if (privacyStorageFeature != PrivacyStorageFeature.ALL) { - keysByPrivacyStorageFeature[privacyStorageFeature]?.forEach { - remove(it) - } - } else { - clear() + internal fun cleanStorageFeature(privacyStorageFeature: PrivacyStorageFeature) = prefs.edit().apply { + if (privacyStorageFeature != PrivacyStorageFeature.ALL) { + keysByPrivacyStorageFeature[privacyStorageFeature]?.forEach { + remove(it) } - }.apply() + } else { + clear() + } + }.apply() init { if (REMOVED_KEYS.any { prefs.contains(it) }) { diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/Utils.kt b/piano-analytics/src/main/java/io/piano/android/analytics/Utils.kt index b3bba9d..e098773 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/Utils.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/Utils.kt @@ -12,13 +12,12 @@ internal fun isLogHttpSet(): Boolean = getProperty(LOG_HTTP_KEY) == "true" @SuppressLint("PrivateApi") @Suppress("SameParameterValue") -private fun getProperty(key: String): String? = - runCatching { - Class.forName("android.os.SystemProperties") - .getMethod("get", String::class.java, String::class.java) - .invoke(null, key, null) as? String - }.onFailure { - Timber.w(it, "can't get value %s from SystemProperties", key) - }.getOrNull() +private fun getProperty(key: String): String? = runCatching { + Class.forName("android.os.SystemProperties") + .getMethod("get", String::class.java, String::class.java) + .invoke(null, key, null) as? String +}.onFailure { + Timber.w(it, "can't get value %s from SystemProperties", key) +}.getOrNull() private const val LOG_HTTP_KEY = "debug.piano.sdk" diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/GroupEventProcessor.kt b/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/GroupEventProcessor.kt index 934e86e..342e47a 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/GroupEventProcessor.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/GroupEventProcessor.kt @@ -5,8 +5,7 @@ import io.piano.android.analytics.model.Event internal class GroupEventProcessor internal constructor( private val processors: MutableList = mutableListOf(), ) : EventProcessor, MutableList by processors { - override fun process(events: List): List = - processors.fold(events) { acc, eventProcessor -> - eventProcessor.process(acc) - } + override fun process(events: List): List = processors.fold(events) { acc, eventProcessor -> + eventProcessor.process(acc) + } } diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/PrivacyEventProcessor.kt b/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/PrivacyEventProcessor.kt index 7dcf7cd..61a4c75 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/PrivacyEventProcessor.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/PrivacyEventProcessor.kt @@ -47,12 +47,11 @@ internal class PrivacyEventProcessor( } @Suppress("NOTHING_TO_INLINE") - private inline fun Set.simplify(): Set = - if (contains(WILDCARD)) { - setOf(WILDCARD) - } else { - toSet() - } + private inline fun Set.simplify(): Set = if (contains(WILDCARD)) { + setOf(WILDCARD) + } else { + toSet() + } @Suppress("NOTHING_TO_INLINE") private inline fun Map>.simplify(): Map> = mapValues { diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/UserEventProcessor.kt b/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/UserEventProcessor.kt index 16113c2..18d95e8 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/UserEventProcessor.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/eventprocessors/UserEventProcessor.kt @@ -8,17 +8,16 @@ import io.piano.android.analytics.model.PropertyName internal class UserEventProcessor( private val userStorage: UserStorage, ) : EventProcessor { - override fun process(events: List): List = - userStorage.currentUser?.let { - val properties = mutableListOf( - Property(PropertyName.USER_ID, it.id), - Property(PropertyName.USER_RECOGNITION, userStorage.userRecognized) - ) - if (it.category != null) { - properties.add(Property(PropertyName.USER_CATEGORY, it.category)) - } - events.map { event -> - event.newBuilder().properties(properties).build() - } - } ?: events + override fun process(events: List): List = userStorage.currentUser?.let { + val properties = mutableListOf( + Property(PropertyName.USER_ID, it.id), + Property(PropertyName.USER_RECOGNITION, userStorage.userRecognized) + ) + if (it.category != null) { + properties.add(Property(PropertyName.USER_CATEGORY, it.category)) + } + events.map { event -> + event.newBuilder().properties(properties).build() + } + } ?: events } diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/idproviders/UuidIdProvider.kt b/piano-analytics/src/main/java/io/piano/android/analytics/idproviders/UuidIdProvider.kt index 5fe4eb2..8478b70 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/idproviders/UuidIdProvider.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/idproviders/UuidIdProvider.kt @@ -33,11 +33,10 @@ internal class UuidIdProvider( } override val isLimitAdTrackingEnabled: Boolean = false - internal fun createNewUuid() = - UUID.randomUUID().toString().also { - prefsStorage.visitorUuid = it - prefsStorage.visitorUuidGenerateTimestamp = getGenerationTimestamp() - } + internal fun createNewUuid() = UUID.randomUUID().toString().also { + prefsStorage.visitorUuid = it + prefsStorage.visitorUuidGenerateTimestamp = getGenerationTimestamp() + } // for mocking in tests internal fun getGenerationTimestamp() = System.currentTimeMillis() From 7007742334fee3a46ff6e5061e9926403475345b Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Wed, 25 Oct 2023 13:56:49 +0400 Subject: [PATCH 4/6] Add support for forcing property type for backend --- app/build.gradle.kts | 2 +- gradle/libs.versions.toml | 6 +-- piano-analytics/build.gradle.kts | 2 +- .../analytics/EventPropertiesJsonAdapter.kt | 23 ++++++++++- .../piano/android/analytics/model/Property.kt | 41 ++++++++++++++----- 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 916ea66..4aa5f40 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,7 +6,7 @@ plugins { android { defaultConfig { minSdk = 21 - compileSdk = 33 + compileSdk = 34 targetSdk = 34 applicationId = "com.example.piano_analytics_android" versionCode = 1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ef5819b..9a0d687 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,9 +10,8 @@ moshiIR = "0.22.1" # AndroidX libraries compatLibrary = "1.6.1" -annotationsLibrary = "1.6.0" lifecycle = "2.6.2" -materialLibrary = "1.9.0" +materialLibrary = "1.10.0" # Third party Libraries googleAdsId = "18.0.1" @@ -27,7 +26,7 @@ viewBindingProperty = "1.5.9" junit = "4.13.2" androidxTestCore = "1.5.0" mockitoKotlin = "2.2.0" -mockitoCore = "5.5.0" +mockitoCore = "5.6.0" [plugins] android-library = { id = "com.android.library", version.ref = "android" } @@ -40,7 +39,6 @@ moshiIR = { id = "dev.zacsweers.moshix", version.ref = "moshiIR"} versionUpdater = { id = "com.github.ben-manes.versions", version.ref = "versionUpdater" } [libraries] -annotations = { module = "androidx.annotation:annotation", version.ref = "annotationsLibrary" } appcompat = { module = "androidx.appcompat:appcompat", version.ref = "compatLibrary" } lifecycleProcess = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle"} material = { module = "com.google.android.material:material", version.ref = "materialLibrary" } diff --git a/piano-analytics/build.gradle.kts b/piano-analytics/build.gradle.kts index f18dd9f..4303d46 100644 --- a/piano-analytics/build.gradle.kts +++ b/piano-analytics/build.gradle.kts @@ -18,7 +18,7 @@ version = VERSION_NAME android { defaultConfig { minSdk = 21 - compileSdk = 33 + compileSdk = 34 buildConfigField("String", "SDK_VERSION", """"${project.version}"""") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/EventPropertiesJsonAdapter.kt b/piano-analytics/src/main/java/io/piano/android/analytics/EventPropertiesJsonAdapter.kt index 5b19b21..3806c91 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/EventPropertiesJsonAdapter.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/EventPropertiesJsonAdapter.kt @@ -20,7 +20,17 @@ internal class EventPropertiesJsonAdapter( is Double, is Boolean, is Array<*>, - -> Property(PropertyName(entry.key), entry.value) + -> { + val delimiterIndex = entry.key.lastIndexOf(DELIMITER) + val key = entry.key.substring(delimiterIndex + 1) + val type = if (delimiterIndex != -1) { + val prefix = entry.key.substring(0, delimiterIndex) + Property.Type.values().firstOrNull { it.prefix == prefix } + } else { + null + } + Property(PropertyName(key), entry.value, type) + } else -> null } @@ -29,6 +39,15 @@ internal class EventPropertiesJsonAdapter( override fun toJson(writer: JsonWriter, value: Set?) { requireNotNull(value) - mapAdapter.toJson(writer, value.associate { it.name.key.lowercase() to it.value }) + mapAdapter.toJson( + writer, + value.associate { + it.forceType?.prefix?.plus(DELIMITER).orEmpty() + it.name.key.lowercase() to it.value + } + ) + } + + companion object { + private const val DELIMITER = ":" } } diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/model/Property.kt b/piano-analytics/src/main/java/io/piano/android/analytics/model/Property.kt index 7fba801..07ce45d 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/model/Property.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/model/Property.kt @@ -8,6 +8,7 @@ import java.util.Date class Property { val name: PropertyName val value: Any + val forceType: Type? override fun equals(other: Any?): Boolean { if (this === other) return true @@ -17,10 +18,11 @@ class Property { override fun hashCode(): Int = name.key.lowercase().hashCode() - internal constructor(name: PropertyName, value: Any) { + internal constructor(name: PropertyName, value: Any, forceType: Type? = null) { require(name != PropertyName.ANY_PROPERTY) this.name = name this.value = value + this.forceType = forceType } /** @@ -29,10 +31,11 @@ class Property { * @param name property name * @param value property value */ - constructor(name: PropertyName, value: String) { + constructor(name: PropertyName, value: String, forceType: Type? = null) { require(name != PropertyName.ANY_PROPERTY) this.name = name this.value = value + this.forceType = forceType } /** @@ -41,10 +44,11 @@ class Property { * @param name property name * @param value property value */ - constructor(name: PropertyName, value: Int) { + constructor(name: PropertyName, value: Int, forceType: Type? = null) { require(name != PropertyName.ANY_PROPERTY) this.name = name this.value = value + this.forceType = forceType } /** @@ -53,10 +57,11 @@ class Property { * @param name property name * @param value property value */ - constructor(name: PropertyName, value: Long) { + constructor(name: PropertyName, value: Long, forceType: Type? = null) { require(name != PropertyName.ANY_PROPERTY) this.name = name this.value = value + this.forceType = forceType } /** @@ -65,10 +70,11 @@ class Property { * @param name property name * @param value property value */ - constructor(name: PropertyName, value: Double) { + constructor(name: PropertyName, value: Double, forceType: Type? = null) { require(name != PropertyName.ANY_PROPERTY) this.name = name this.value = value + this.forceType = forceType } /** @@ -77,7 +83,7 @@ class Property { * @param name property name * @param value property value */ - constructor(name: PropertyName, value: Date) : this(name, value.time / 1000) + constructor(name: PropertyName, value: Date) : this(name, value.time / 1000, Type.DATE) /** * Creates a new property @@ -85,10 +91,11 @@ class Property { * @param name property name * @param value property value */ - constructor(name: PropertyName, value: Boolean) { + constructor(name: PropertyName, value: Boolean, forceType: Type? = null) { require(name != PropertyName.ANY_PROPERTY) this.name = name this.value = value + this.forceType = forceType } /** @@ -97,10 +104,11 @@ class Property { * @param name property name * @param value property value */ - constructor(name: PropertyName, value: Array) { + constructor(name: PropertyName, value: Array, forceType: Type? = null) { require(name != PropertyName.ANY_PROPERTY) this.name = name this.value = value + this.forceType = forceType } /** @@ -109,10 +117,11 @@ class Property { * @param name property name * @param value property value */ - constructor(name: PropertyName, value: Array) { + constructor(name: PropertyName, value: Array, forceType: Type? = null) { require(name != PropertyName.ANY_PROPERTY) this.name = name this.value = value + this.forceType = forceType } /** @@ -121,9 +130,21 @@ class Property { * @param name property name * @param value property value */ - constructor(name: PropertyName, value: Array) { + constructor(name: PropertyName, value: Array, forceType: Type? = null) { require(name != PropertyName.ANY_PROPERTY) this.name = name this.value = value + this.forceType = forceType + } + + enum class Type(val prefix: String) { + STRING("s"), + INTEGER("n"), + FLOAT("f"), + DATE("d"), + BOOLEAN("b"), + STRING_ARRAY("a:s"), + INTEGER_ARRAY("a:n"), + FLOAT_ARRAY("a:f"), } } From e7f48974874efbcfe517346848f5ba154c92bbfa Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Thu, 26 Oct 2023 14:31:30 +0400 Subject: [PATCH 5/6] Add limit for storing events --- CHANGELOG.md | 1 + .../java/io/piano/android/analytics/EventRepository.kt | 9 +++++++++ .../src/main/java/io/piano/android/analytics/SendTask.kt | 2 ++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae33ef9..9e48929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Piano Analytics SDK for Android ## v3.3.4-SNAPSHOT +* Added limit for event storage * Updated dependencies: - com.squareup.okhttp3:okhttp [4.11.0 -> 4.12.0] https://square.github.io/okhttp/ diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/EventRepository.kt b/piano-analytics/src/main/java/io/piano/android/analytics/EventRepository.kt index d7aeda4..c12ed4e 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/EventRepository.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/EventRepository.kt @@ -22,6 +22,15 @@ internal class EventRepository( ) } + fun deleteOutOfLimitNotSentEvents(limit: Int) { + databaseHelper.delete( + "${EventRecord.IS_SENT} = 0 AND ${EventRecord.ID} <= (" + + "SELECT ${EventRecord.ID} FROM ${EventRecord.TABLE_NAME} WHERE ${EventRecord.IS_SENT} = 0 " + + "ORDER by ${EventRecord.ID} DESC LIMIT 1 OFFSET ?)", + limit.toString() + ) + } + fun getNotSentEvents(): List = databaseHelper.query( selection = "${EventRecord.IS_SENT} = 0", orderBy = "${EventRecord.TIME} ASC" diff --git a/piano-analytics/src/main/java/io/piano/android/analytics/SendTask.kt b/piano-analytics/src/main/java/io/piano/android/analytics/SendTask.kt index 8a1271e..94ece9b 100644 --- a/piano-analytics/src/main/java/io/piano/android/analytics/SendTask.kt +++ b/piano-analytics/src/main/java/io/piano/android/analytics/SendTask.kt @@ -24,6 +24,7 @@ internal class SendTask( ) : Runnable { override fun run() { eventRepository.deleteOldEvents(configuration.eventsOfflineStorageLifetime) + eventRepository.deleteOutOfLimitNotSentEvents(EVENTS_LIMIT) if (deviceInfoProvider.connectionType == ConnectionType.OFFLINE) { Timber.w("Can't send events - no connection") return @@ -61,5 +62,6 @@ internal class SendTask( companion object { internal val MEDIA_TYPE by lazy(LazyThreadSafetyMode.NONE) { "application/json; charset=UTF-8".toMediaType() } private const val CHUNK_SIZE = 50 + private const val EVENTS_LIMIT = 2000 } } From e0f3e161901edb969bb8b440efb2724910ff337c Mon Sep 17 00:00:00 2001 From: Dmitriy Date: Thu, 26 Oct 2023 14:39:07 +0400 Subject: [PATCH 6/6] Prepare release --- CHANGELOG.md | 2 +- piano-analytics/gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e48929..618da9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Piano Analytics SDK for Android -## v3.3.4-SNAPSHOT +## v3.3.4 * Added limit for event storage * Updated dependencies: - com.squareup.okhttp3:okhttp [4.11.0 -> 4.12.0] diff --git a/piano-analytics/gradle.properties b/piano-analytics/gradle.properties index 5f574cd..1166862 100644 --- a/piano-analytics/gradle.properties +++ b/piano-analytics/gradle.properties @@ -1,4 +1,4 @@ -VERSION_NAME=3.3.4-SNAPSHOT +VERSION_NAME=3.3.4 GROUP=io.piano.android POM_NAME=Analytics POM_ARTIFACT_ID=analytics