Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 31 additions & 104 deletions android/src/main/java/com/segment/analytics/kotlin/android/Storage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,125 +5,52 @@ import android.content.SharedPreferences
import com.segment.analytics.kotlin.android.utilities.AndroidKVS
import com.segment.analytics.kotlin.core.Analytics
import com.segment.analytics.kotlin.core.Storage
import com.segment.analytics.kotlin.core.Storage.Companion.MAX_PAYLOAD_SIZE
import com.segment.analytics.kotlin.core.StorageProvider
import com.segment.analytics.kotlin.core.System
import com.segment.analytics.kotlin.core.UserInfo
import com.segment.analytics.kotlin.core.utilities.EventsFileManager
import com.segment.analytics.kotlin.core.utilities.FileEventStream
import com.segment.analytics.kotlin.core.utilities.StorageImpl
import kotlinx.coroutines.CoroutineDispatcher
import sovran.kotlin.Store
import sovran.kotlin.Subscriber
import java.io.File

// Android specific
@Deprecated("Use StorageProvider to create storage for Android instead")
class AndroidStorage(
context: Context,
private val store: Store,
writeKey: String,
private val ioDispatcher: CoroutineDispatcher,
directory: String? = null,
subject: String? = null
) : Subscriber, Storage {
) : StorageImpl(
propertiesFile = AndroidKVS(context.getSharedPreferences("analytics-android-$writeKey", Context.MODE_PRIVATE)),
eventStream = FileEventStream(context.getDir(directory ?: "segment-disk-queue", Context.MODE_PRIVATE)),
store = store,
writeKey = writeKey,
fileIndexKey = if(subject == null) "segment.events.file.index.$writeKey" else "segment.events.file.index.$writeKey.$subject",
ioDispatcher = ioDispatcher
)

private val sharedPreferences: SharedPreferences =
context.getSharedPreferences("analytics-android-$writeKey", Context.MODE_PRIVATE)
override val storageDirectory: File = context.getDir(directory ?: "segment-disk-queue", Context.MODE_PRIVATE)
internal val eventsFile =
EventsFileManager(storageDirectory, writeKey, AndroidKVS(sharedPreferences), subject)

override suspend fun subscribeToStore() {
store.subscribe(
this,
UserInfo::class,
initialState = true,
handler = ::userInfoUpdate,
queue = ioDispatcher
)
store.subscribe(
this,
System::class,
initialState = true,
handler = ::systemUpdate,
queue = ioDispatcher
)
}

override suspend fun write(key: Storage.Constants, value: String) {
when (key) {
Storage.Constants.Events -> {
if (value.length < MAX_PAYLOAD_SIZE) {
// write to disk
eventsFile.storeEvent(value)
} else {
throw Exception("enqueued payload is too large")
}
}
else -> {
sharedPreferences.edit().putString(key.rawVal, value).apply()
}
}
}

/**
* @returns the String value for the associated key
* for Constants.Events it will return a file url that can be used to read the contents of the events
*/
override fun read(key: Storage.Constants): String? {
return when (key) {
Storage.Constants.Events -> {
eventsFile.read().joinToString()
}
Storage.Constants.LegacyAppBuild -> {
// The legacy app build number was stored as an integer so we have to get it
// as an integer and convert it to a String.
val noBuild = -1
val build = sharedPreferences.getInt(key.rawVal, noBuild)
if (build != noBuild) {
return build.toString()
} else {
return null
}
}
else -> {
sharedPreferences.getString(key.rawVal, null)
}
}
}

override fun remove(key: Storage.Constants): Boolean {
return when (key) {
Storage.Constants.Events -> {
true
}
else -> {
sharedPreferences.edit().putString(key.rawVal, null).apply()
true
}
object AndroidStorageProvider : StorageProvider {
override fun createStorage(vararg params: Any): Storage {

if (params.size < 2 || params[0] !is Analytics || params[1] !is Context) {
throw IllegalArgumentException("""
Invalid parameters for AndroidStorageProvider.
AndroidStorageProvider requires at least 2 parameters.
The first argument has to be an instance of Analytics,
an the second argument has to be an instance of Context
""".trimIndent())
}
}

override fun removeFile(filePath: String): Boolean {
return eventsFile.remove(filePath)
}
val analytics = params[0] as Analytics
val context = params[1] as Context
val config = analytics.configuration

override suspend fun rollover() {
eventsFile.rollover()
}
}
val eventDirectory = context.getDir("segment-disk-queue", Context.MODE_PRIVATE)
val fileIndexKey = "segment.events.file.index.${config.writeKey}"
val sharedPreferences: SharedPreferences =
context.getSharedPreferences("analytics-android-${config.writeKey}", Context.MODE_PRIVATE)

object AndroidStorageProvider : StorageProvider {
override fun getStorage(
analytics: Analytics,
store: Store,
writeKey: String,
ioDispatcher: CoroutineDispatcher,
application: Any
): Storage {
return AndroidStorage(
store = store,
writeKey = writeKey,
ioDispatcher = ioDispatcher,
context = application as Context,
)
val propertiesFile = AndroidKVS(sharedPreferences)
val eventStream = FileEventStream(eventDirectory)
return StorageImpl(propertiesFile, eventStream, analytics.store, config.writeKey, fileIndexKey, analytics.fileIODispatcher)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@ import com.segment.analytics.kotlin.core.utilities.KVS
/**
* A key-value store wrapper for sharedPreferences on Android
*/
class AndroidKVS(val sharedPreferences: SharedPreferences) : KVS {
override fun getInt(key: String, defaultVal: Int): Int =
class AndroidKVS(val sharedPreferences: SharedPreferences): KVS {


override fun get(key: String, defaultVal: Int) =
sharedPreferences.getInt(key, defaultVal)

override fun putInt(key: String, value: Int): Boolean =
override fun get(key: String, defaultVal: String?) =
sharedPreferences.getString(key, defaultVal) ?: defaultVal

override fun put(key: String, value: Int) =
sharedPreferences.edit().putInt(key, value).commit()

override fun put(key: String, value: String) =
sharedPreferences.edit().putString(key, value).commit()

override fun remove(key: String): Boolean =
sharedPreferences.edit().remove(key).commit()

override fun contains(key: String) = sharedPreferences.contains(key)
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,24 +149,5 @@ class AndroidContextCollectorTests {
}
}



@Test
fun `storage directory can be customized`() {
val dir = "test"
val androidStorage = AndroidStorage(
appContext,
Store(),
"123",
UnconfinedTestDispatcher(),
dir
)

Assertions.assertTrue(androidStorage.storageDirectory.name.contains(dir))
Assertions.assertTrue(androidStorage.eventsFile.directory.name.contains(dir))
Assertions.assertTrue(androidStorage.storageDirectory.exists())
Assertions.assertTrue(androidStorage.eventsFile.directory.exists())
}

private fun JsonElement?.asString(): String? = this?.jsonPrimitive?.content
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class StorageTests {
@Nested
inner class Android {
private var store = Store()
private lateinit var androidStorage: AndroidStorage
private lateinit var androidStorage: Storage
private var mockContext: Context = mockContext()

init {
Expand Down Expand Up @@ -74,7 +74,7 @@ class StorageTests {
"123",
UnconfinedTestDispatcher()
)
androidStorage.subscribeToStore()
androidStorage.initialize()
}


Expand Down Expand Up @@ -208,9 +208,12 @@ class StorageTests {
}
val stringified: String = Json.encodeToString(event)
androidStorage.write(Storage.Constants.Events, stringified)
androidStorage.eventsFile.rollover()
val storagePath = androidStorage.eventsFile.read()[0]
val storageContents = File(storagePath).readText()
androidStorage.rollover()
val storagePath = androidStorage.read(Storage.Constants.Events)?.let{
it.split(',')[0]
}
assertNotNull(storagePath)
val storageContents = File(storagePath!!).readText()
val jsonFormat = Json.decodeFromString(JsonObject.serializer(), storageContents)
assertEquals(1, jsonFormat["batch"]!!.jsonArray.size)
}
Expand All @@ -229,8 +232,8 @@ class StorageTests {
e
}
assertNotNull(exception)
androidStorage.eventsFile.rollover()
assertTrue(androidStorage.eventsFile.read().isEmpty())
androidStorage.rollover()
assertTrue(androidStorage.read(Storage.Constants.Events).isNullOrEmpty())
}

@Test
Expand All @@ -248,7 +251,7 @@ class StorageTests {
val stringified: String = Json.encodeToString(event)
androidStorage.write(Storage.Constants.Events, stringified)

androidStorage.eventsFile.rollover()
androidStorage.rollover()
val fileUrl = androidStorage.read(Storage.Constants.Events)
assertNotNull(fileUrl)
fileUrl!!.let {
Expand All @@ -270,7 +273,7 @@ class StorageTests {

@Test
fun `reading events with empty storage return empty list`() = runTest {
androidStorage.eventsFile.rollover()
androidStorage.rollover()
val fileUrls = androidStorage.read(Storage.Constants.Events)
assertTrue(fileUrls!!.isEmpty())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.segment.analytics.kotlin.android.utilities

import com.segment.analytics.kotlin.android.utils.MemorySharedPreferences
import com.segment.analytics.kotlin.core.utilities.KVS
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class AndroidKVSTest {

private lateinit var prefs: KVS

@BeforeEach
fun setup(){
val sharedPreferences = MemorySharedPreferences()
prefs = AndroidKVS(sharedPreferences)
prefs.put("int", 1)
prefs.put("string", "string")
}

@Test
fun getTest() {
Assertions.assertEquals(1, prefs.get("int", 0))
Assertions.assertEquals("string", prefs.get("string", null))
Assertions.assertEquals(0, prefs.get("keyNotExists", 0))
Assertions.assertEquals(null, prefs.get("keyNotExists", null))
}

@Test
fun putTest() {
prefs.put("int", 2)
prefs.put("string", "stringstring")

Assertions.assertEquals(2, prefs.get("int", 0))
Assertions.assertEquals("stringstring", prefs.get("string", null))
}

@Test
fun containsAndRemoveTest() {
Assertions.assertTrue(prefs.contains("int"))
prefs.remove("int")
Assertions.assertFalse(prefs.contains("int"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,8 @@ open class Analytics protected constructor(
}

// use lazy to avoid the instance being leak before fully initialized
val storage: Storage by lazy {
configuration.storageProvider.getStorage(
analytics = this,
writeKey = configuration.writeKey,
ioDispatcher = fileIODispatcher,
store = store,
application = configuration.application!!
)
open val storage: Storage by lazy {
configuration.storageProvider.createStorage(this, configuration.application!!)
}

internal var userInfo: UserInfo = UserInfo.defaultState(storage)
Expand Down Expand Up @@ -134,7 +128,7 @@ open class Analytics protected constructor(
it.provide(System.defaultState(configuration, storage))

// subscribe to store after state is provided
storage.subscribeToStore()
storage.initialize()
Telemetry.subscribe(store)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import sovran.kotlin.Store
data class Configuration(
val writeKey: String,
var application: Any? = null,
val storageProvider: StorageProvider = ConcreteStorageProvider,
var storageProvider: StorageProvider = ConcreteStorageProvider,
var collectDeviceId: Boolean = false,
var trackApplicationLifecycleEvents: Boolean = false,
var useLifecycleObserver: Boolean = false,
Expand Down
Loading
Loading