Skip to content

IAF | Persisting the webview after close #270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 29, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ sealed class ProfileKey(name: String) : Keyword(name) {

// Push properties
internal object PUSH_TOKEN : ProfileKey("push_token")
internal object PUSH_STATE : ProfileKey("push_state")

// Personal information
object FIRST_NAME : ProfileKey("first_name")
Expand Down Expand Up @@ -56,5 +55,3 @@ sealed class ProfileKey(name: String) : Keyword(name) {
}

internal object PROFILE_ATTRIBUTES : Keyword("attributes")

internal object API_KEY : Keyword("api_key")
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.klaviyo.analytics.model

sealed class StateKey(name: String) : Keyword(name) {
/**
* Key in state for storing the company ID aka public API key
*/
object API_KEY : StateKey("api_key")

/**
* Key in state for storing the latest push status
*/
internal object PUSH_STATE : StateKey("push_state")
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.klaviyo.analytics.networking

import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import com.klaviyo.analytics.model.Event
import com.klaviyo.analytics.model.EventMetric
import com.klaviyo.analytics.model.Profile
Expand Down Expand Up @@ -30,7 +28,9 @@ import org.json.JSONObject
internal object KlaviyoApiClient : ApiClient {
internal const val QUEUE_KEY = "klaviyo_api_request_queue"

private var handlerThread = HandlerUtil.getHandlerThread(KlaviyoApiClient::class.simpleName)
private var handlerThread = Registry.threadHelper.getHandlerThread(
KlaviyoApiClient::class.simpleName
)
private var handler: Handler? = null
private var apiQueue = ConcurrentLinkedDeque<KlaviyoApiRequest>()
private var queueInitialized = false
Expand Down Expand Up @@ -258,12 +258,14 @@ internal object KlaviyoApiClient : ApiClient {
private fun initBatch() {
synchronized(this) {
if (handlerThread.state == Thread.State.TERMINATED) {
handlerThread = HandlerUtil.getHandlerThread(KlaviyoApiClient::class.simpleName)
handlerThread = Registry.threadHelper.getHandlerThread(
KlaviyoApiClient::class.simpleName
)
}

if (handlerThread.state == Thread.State.NEW) {
handlerThread.start()
handler = HandlerUtil.getHandler(handlerThread.looper)
handler = Registry.threadHelper.getHandler(handlerThread.looper)
}
}

Expand Down Expand Up @@ -374,12 +376,4 @@ internal object KlaviyoApiClient : ApiClient {
handler?.postDelayed(this, flushInterval)
}
}

/**
* Abstraction of our interactions with handlers/threads for isolation purposes
*/
internal object HandlerUtil {
fun getHandler(looper: Looper) = Handler(looper)
fun getHandlerThread(name: String?) = HandlerThread(name)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.klaviyo.analytics.state

import com.klaviyo.analytics.model.API_KEY
import com.klaviyo.analytics.model.ImmutableProfile
import com.klaviyo.analytics.model.PROFILE_ATTRIBUTES
import com.klaviyo.analytics.model.Profile
Expand All @@ -10,8 +9,9 @@ import com.klaviyo.analytics.model.ProfileKey.Companion.IDENTIFIERS
import com.klaviyo.analytics.model.ProfileKey.EMAIL
import com.klaviyo.analytics.model.ProfileKey.EXTERNAL_ID
import com.klaviyo.analytics.model.ProfileKey.PHONE_NUMBER
import com.klaviyo.analytics.model.ProfileKey.PUSH_STATE
import com.klaviyo.analytics.model.ProfileKey.PUSH_TOKEN
import com.klaviyo.analytics.model.StateKey.API_KEY
import com.klaviyo.analytics.model.StateKey.PUSH_STATE
import com.klaviyo.analytics.networking.requests.PushTokenApiRequest
import com.klaviyo.core.Registry
import java.io.Serializable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.klaviyo.analytics.state

import com.klaviyo.analytics.model.API_KEY
import com.klaviyo.analytics.model.ImmutableProfile
import com.klaviyo.analytics.model.Keyword
import com.klaviyo.analytics.model.PROFILE_ATTRIBUTES
import com.klaviyo.analytics.model.ProfileKey
import com.klaviyo.analytics.model.StateKey.API_KEY

sealed interface StateChange {
val key: Keyword?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.klaviyo.analytics.state
import com.klaviyo.analytics.model.ImmutableProfile
import com.klaviyo.analytics.model.Profile
import com.klaviyo.analytics.model.ProfileKey
import com.klaviyo.analytics.model.StateKey
import com.klaviyo.analytics.networking.ApiClient
import com.klaviyo.analytics.networking.requests.ApiRequest
import com.klaviyo.analytics.networking.requests.KlaviyoApiRequest
Expand Down Expand Up @@ -162,7 +163,7 @@ internal class StateSideEffects(
}

is StateChange.KeyValue -> when (change.key) {
ProfileKey.PUSH_STATE -> onPushStateChange()
StateKey.PUSH_STATE -> onPushStateChange()
ProfileKey.PUSH_TOKEN -> Unit /* Token is a no-op, push changes are captured by push state */
else -> Unit
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package com.klaviyo.analytics.networking

import android.os.Handler
import android.os.HandlerThread
import com.klaviyo.analytics.model.Event
import com.klaviyo.analytics.model.EventMetric
import com.klaviyo.analytics.model.Profile
import com.klaviyo.analytics.networking.KlaviyoApiClient.HandlerUtil
import com.klaviyo.analytics.networking.requests.ApiRequest
import com.klaviyo.analytics.networking.requests.EventApiRequest
import com.klaviyo.analytics.networking.requests.KlaviyoApiRequest
Expand Down Expand Up @@ -62,7 +59,6 @@ internal class KlaviyoApiClientTest : BaseTest() {
private companion object {
private val slotOnActivityEvent = slot<ActivityObserver>()
private val slotOnNetworkChange = slot<NetworkObserver>()
private val mockHandler = mockk<Handler>()
}

@Before
Expand All @@ -89,22 +85,9 @@ internal class KlaviyoApiClientTest : BaseTest() {
every { mockNetworkMonitor.onNetworkChange(capture(slotOnNetworkChange)) } returns Unit
every { mockNetworkMonitor.offNetworkChange(capture(slotOnNetworkChange)) } returns Unit

mockkObject(HandlerUtil)
every { HandlerUtil.getHandler(any()) } returns mockHandler.apply {
every { removeCallbacksAndMessages(any()) } returns Unit
every { post(any()) } answers { a ->
postedJob = (a.invocation.args[0] as KlaviyoApiClient.NetworkRunnable)
postedJob!!.run().let { true }
}
every { postDelayed(any(), any()) } answers { a ->
postedJob = a.invocation.args[0] as KlaviyoApiClient.NetworkRunnable
true
}
}
every { HandlerUtil.getHandlerThread(any()) } returns mockk<HandlerThread>().apply {
every { start() } returns Unit
every { looper } returns mockk()
every { state } returns Thread.State.NEW
every { mockHandler.postDelayed(any(), any()) } answers {
postedJob = firstArg<KlaviyoApiClient.NetworkRunnable>()
true
}

KlaviyoApiClient.startService()
Expand All @@ -118,7 +101,6 @@ internal class KlaviyoApiClientTest : BaseTest() {
super.cleanup()
unmockkObject(KlaviyoApiClient)
unmockkObject(KlaviyoApiRequestDecoder)
unmockkObject(HandlerUtil)
unmockDeviceProperties()
unmockkStatic(DeviceProperties::buildEventMetaData)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.klaviyo.analytics.state

import com.klaviyo.analytics.model.API_KEY as API_KEYWORD
import com.klaviyo.analytics.model.Profile
import com.klaviyo.analytics.model.ProfileKey
import com.klaviyo.analytics.model.StateKey.API_KEY as API_KEYWORD
import com.klaviyo.fixtures.BaseTest
import io.mockk.mockk
import io.mockk.verify
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.klaviyo.analytics.state

import com.klaviyo.analytics.model.Profile
import com.klaviyo.analytics.model.ProfileKey
import com.klaviyo.analytics.model.StateKey
import com.klaviyo.analytics.networking.ApiClient
import com.klaviyo.analytics.networking.ApiObserver
import com.klaviyo.analytics.networking.requests.EventApiRequest
Expand Down Expand Up @@ -196,7 +197,7 @@ class StateSideEffectsTest : BaseTest() {

StateSideEffects(stateMock, apiClientMock)

capturedStateChangeObserver.captured(StateChange.KeyValue(ProfileKey.PUSH_STATE, null))
capturedStateChangeObserver.captured(StateChange.KeyValue(StateKey.PUSH_STATE, null))
verify(exactly = 1) { apiClientMock.enqueuePushToken("token", profile) }
}

Expand All @@ -206,7 +207,7 @@ class StateSideEffectsTest : BaseTest() {

StateSideEffects(stateMock, apiClientMock)

capturedStateChangeObserver.captured(StateChange.KeyValue(ProfileKey.PUSH_STATE, null))
capturedStateChangeObserver.captured(StateChange.KeyValue(StateKey.PUSH_STATE, null))
verify(exactly = 0) { apiClientMock.enqueuePushToken(any(), any()) }
}

Expand Down
16 changes: 10 additions & 6 deletions sdk/core/src/main/java/com/klaviyo/core/Registry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import com.klaviyo.core.model.DataStore
import com.klaviyo.core.model.SharedPreferencesDataStore
import com.klaviyo.core.networking.KlaviyoNetworkMonitor
import com.klaviyo.core.networking.NetworkMonitor
import com.klaviyo.core.utils.KlaviyoThreadHelper
import com.klaviyo.core.utils.ThreadHelper
import kotlin.reflect.KType
import kotlin.reflect.typeOf

Expand Down Expand Up @@ -50,6 +52,8 @@ object Registry {
*/
val configBuilder: Config.Builder get() = KlaviyoConfig.Builder()

val threadHelper: ThreadHelper = KlaviyoThreadHelper

val clock: Clock get() = SystemClock

val log: Log get() = KLog
Expand Down Expand Up @@ -149,8 +153,8 @@ object Registry {

if (service is T) return service

return when (val service = registry[type]?.let { it() }) {
is T -> service.apply { services[type] = service }
return when (val lazyService = registry[type]?.let { it() }) {
is T -> lazyService.apply { services[type] = lazyService }
else -> null
}
}
Expand All @@ -168,18 +172,18 @@ object Registry {

if (service is T) return service

when (val s = registry[type]?.let { it() }) {
when (val lazyService = registry[type]?.let { it() }) {
is T -> {
services[type] = s
return s
services[type] = lazyService
return lazyService
}

is Any -> throw InvalidRegistration(type)
else -> {
if (type == typeOf<Config>()) {
throw MissingConfig()
} else {
throw throw MissingRegistration(type)
throw MissingRegistration(type)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.klaviyo.core.utils

import android.os.Handler
import android.os.HandlerThread
import android.os.Looper

/**
* Abstraction of our interactions with handlers/threads for isolation purposes
* @deprecated Use [com.klaviyo.core.Registry.threadHelper] instead
*/
object KlaviyoThreadHelper : ThreadHelper {
override fun getHandler(looper: Looper) = Handler(looper)
override fun getHandlerThread(name: String?) = HandlerThread(name)
override fun runOnUiThread(job: () -> Unit) {
val mainLooper = Looper.getMainLooper()

if (mainLooper == Looper.myLooper()) {
// Already on main thread, run immediately
job()
} else {
// Post to main thread
getHandler(mainLooper).post(job)
}
}
}
14 changes: 14 additions & 0 deletions sdk/core/src/main/java/com/klaviyo/core/utils/ThreadHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.klaviyo.core.utils

import android.os.Handler
import android.os.HandlerThread
import android.os.Looper

/**
* Abstraction of our interactions with handlers/threads for isolation purposes
*/
interface ThreadHelper {
fun getHandler(looper: Looper): Handler
fun getHandlerThread(name: String?): HandlerThread
fun runOnUiThread(job: () -> Unit)
}
74 changes: 74 additions & 0 deletions sdk/core/src/test/java/com/klaviyo/core/utils/ThreadHelperTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.klaviyo.core.utils

import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import io.mockk.EqMatcher
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkConstructor
import io.mockk.mockkStatic
import io.mockk.unmockkConstructor
import io.mockk.unmockkStatic
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class ThreadHelperTest {
@Before
fun setUp() {
mockkStatic(Looper::class)
mockkConstructor(Handler::class)
mockkConstructor(HandlerThread::class)
}

@After
fun tearDown() {
unmockkStatic(Looper::class)
unmockkConstructor(Handler::class)
unmockkConstructor(HandlerThread::class)
}

@Test
fun `runOnUiThread runs block immediately on main thread`() {
val mainLooper = mockk<Looper>()
every { Looper.getMainLooper() } returns mainLooper
every { Looper.myLooper() } returns mainLooper

var ran = false
KlaviyoThreadHelper.runOnUiThread { ran = true }

assert(ran) {
"Block should have run immediately on main thread"
}
}

@Test
fun `runOnUiThread posts block when not on main thread`() {
val mainLooper = mockk<Looper>()
val otherLooper = mockk<Looper>()
every { Looper.getMainLooper() } returns mainLooper
every { Looper.myLooper() } returns otherLooper

every { constructedWith<Handler>(EqMatcher(mainLooper, true)).post(any()) } answers {
firstArg<Runnable>().run()
true
}

var ran = false
KlaviyoThreadHelper.runOnUiThread { ran = true }

assert(ran) {
"Block should have run after being posted to main thread"
}
}

@Test
fun `getHandlerThread returns a HandlerThread for that name`() {
val name = "TestThread"
every { constructedWith<HandlerThread>(EqMatcher(name, true)).name } returns name
val result = KlaviyoThreadHelper.getHandlerThread(name)
assertEquals(name, result.name)
}
}
Loading