Skip to content
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

refactor: refactor navcontroller tracking plugin teardown #70

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1293af4
feat: add shutdown method
Nov 13, 2024
4bba398
refactor: add shutdown check in public apis
Nov 13, 2024
f0efefb
test: add test in RudderStackDataplanePluginTest
Nov 13, 2024
27bdb92
feat: add shutdown button in sample app
Nov 13, 2024
d32897e
refactor: make isAnalyticsShutdown setter private
Nov 13, 2024
c1b0613
refactor: move stop function to teardown
Nov 15, 2024
aec8dbf
refactor: refactor shutdown logic
Nov 15, 2024
80e590c
refactor: return empty string for shutdown in getAnonymousId
Nov 15, 2024
3b6e919
test: update MessageQueueTest and RudderStackDataplanePluginTest
Nov 15, 2024
16b1a2a
refactor: move the removeLifecycleObserver calls to respective plugins
Nov 18, 2024
af0ba11
test: update tests regarding teardown
Nov 19, 2024
21c661e
refactor: change shutdown logic
Nov 20, 2024
55643da
chore: add comment for ensureActive
Nov 20, 2024
e2f3061
refactor: remove coroutine call from teardown of RudderStackDataplane…
Nov 20, 2024
5d76584
refactor: remove unncessary super.teardown call
Nov 20, 2024
8886ca7
Merge branch 'develop.poc' into feat/sdk-2599-add-shutdown
Nov 20, 2024
6e6411c
refactor: add shutdown check for reset
Nov 20, 2024
d241782
chore: add comment for removing VisibleForTesting
Nov 20, 2024
992efbc
refactor: add analytics shutdown check in reset
Nov 23, 2024
216873e
refactor: add analytics shutdown check in session APIs
Nov 23, 2024
82e4b78
feat: add initialize sdk button
Nov 23, 2024
036cbf4
chore: add todo for analyticsJob
Nov 23, 2024
62c434a
test: update MessageQueueTest to remove VisibleForTesting from Messag…
Nov 25, 2024
f417361
refactor: revert write key and data plane url
Nov 25, 2024
c6080bb
refactor: refactor NavControllerTrackingPlugin teardown to remove act…
Nov 26, 2024
6cc8b6e
test: update NavControllerActivityObserverTest
Nov 26, 2024
a94cd8f
test: update NavControllerTrackingPluginTest
Nov 26, 2024
8d3a3f7
Merge branch 'develop.poc' into refactor/sdk-2674-refactor-navcontrol…
Nov 27, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import com.rudderstack.android.sdk.state.NavContext
import com.rudderstack.android.sdk.utils.runOnMainThread
import kotlinx.coroutines.DelicateCoroutinesApi
import java.util.concurrent.atomic.AtomicBoolean
import com.rudderstack.android.sdk.Analytics as AndroidAnalytics

/*
* This class is used to attach an observer to a navController's activity, so that destination changes can be tracked
* when navigating back from a previous activity or when the app is foregrounded.
* */
@OptIn(DelicateCoroutinesApi::class)
internal class NavControllerActivityObserver(
private val plugin: NavControllerTrackingPlugin,
private val navContext: NavContext,
) : DefaultLifecycleObserver {

private val isActivityGettingCreated = AtomicBoolean(true)

init {
activityLifecycle()?.addObserver(this)
}
fun find(navContext: NavContext) = navContext == this.navContext

override fun onStart(owner: LifecycleOwner) {
if (!isActivityGettingCreated.getAndSet(false)) {
Expand All @@ -37,10 +39,21 @@ internal class NavControllerActivityObserver(
}

override fun onDestroy(owner: LifecycleOwner) {
activityLifecycle()?.removeObserver(this)
plugin.navContextState.dispatch(NavContext.RemoveNavContextAction(navContext))
}

internal fun removeObserver() {
(plugin.analytics as? AndroidAnalytics)?.runOnMainThread {
activityLifecycle()?.removeObserver(this)
}
}

internal fun addObserver() {
(plugin.analytics as? AndroidAnalytics)?.runOnMainThread {
activityLifecycle()?.addObserver(this)
}
}

@VisibleForTesting
internal fun activityLifecycle(): Lifecycle? {
return (navContext.callingActivity as? LifecycleOwner)?.lifecycle
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package com.rudderstack.android.sdk.plugins.screenrecording

import android.os.Bundle
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import com.rudderstack.android.sdk.state.NavContext
import com.rudderstack.android.sdk.utils.automaticProperty
import com.rudderstack.kotlin.sdk.Analytics
import com.rudderstack.kotlin.sdk.internals.plugins.Plugin
import com.rudderstack.kotlin.sdk.internals.statemanagement.FlowState
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import com.rudderstack.android.sdk.Configuration as AndroidConfiguration

internal const val FRAGMENT_NAVIGATOR_NAME = "fragment"
Expand All @@ -25,14 +26,15 @@ internal class NavControllerTrackingPlugin(

override lateinit var analytics: Analytics

@VisibleForTesting
internal val currentNavContexts: MutableSet<NavContext> = mutableSetOf()
private val currentNavContexts: MutableSet<NavContext> = mutableSetOf()

private val activityObservers: MutableList<NavControllerActivityObserver> = mutableListOf()

override fun setup(analytics: Analytics) {
super.setup(analytics)
(analytics.configuration as? AndroidConfiguration)?.let {
navContextState.onEach { currentState ->
updateNavContexts(currentState)
withContext(NonCancellable) { updateNavContexts(currentState) }
}.launchIn(analytics.analyticsScope)
}
}
Expand All @@ -58,7 +60,7 @@ internal class NavControllerTrackingPlugin(
private fun removeDeletedNavContexts(updatedNavContexts: Set<NavContext>) {
val deletedNavContexts = currentNavContexts.minus(updatedNavContexts)
deletedNavContexts.forEach { navContext ->
removeContext(navContext)
removeContextAndObserver(navContext)
}
}

Expand All @@ -69,9 +71,13 @@ internal class NavControllerTrackingPlugin(
}
}

private fun removeContext(navContext: NavContext) {
private fun removeContextAndObserver(navContext: NavContext) {
navContext.navController.removeOnDestinationChangedListener(this)
currentNavContexts.remove(navContext)

val observerToBeRemoved = activityObservers.firstOrNull { it.find(navContext) }
observerToBeRemoved?.removeObserver()
observerToBeRemoved?.let { activityObservers.remove(it) }
}

private fun addContextAndObserver(navContext: NavContext) {
Expand All @@ -80,10 +86,12 @@ internal class NavControllerTrackingPlugin(
currentNavContexts.add(navContext)

// adding activity observer
NavControllerActivityObserver(
val observerToBeAdded = provideNavControllerActivityObserver(
plugin = this,
navContext = navContext
)
observerToBeAdded.addObserver()
activityObservers.add(observerToBeAdded)
Comment on lines +89 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may consider using the scope function.

}

private fun trackFragmentScreen(destination: NavDestination) {
Expand All @@ -107,3 +115,10 @@ internal class NavControllerTrackingPlugin(
analytics.screen(screenName = screenName, properties = automaticProperty())
}
}

internal fun provideNavControllerActivityObserver(
plugin: NavControllerTrackingPlugin,
navContext: NavContext
): NavControllerActivityObserver {
return NavControllerActivityObserver(plugin, navContext)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.rudderstack.android.sdk.plugins.screenrecording

import androidx.activity.ComponentActivity
import androidx.navigation.NavDestination
import com.rudderstack.kotlin.sdk.Analytics
import com.rudderstack.android.sdk.state.NavContext
import com.rudderstack.android.sdk.utils.mockAnalytics
import com.rudderstack.kotlin.sdk.internals.statemanagement.FlowState
import io.mockk.MockKAnnotations
import io.mockk.Runs
Expand All @@ -12,12 +14,24 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test

@OptIn(ExperimentalCoroutinesApi::class)
class NavControllerActivityObserverTest {

private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)

@MockK
private lateinit var mockPlugin: NavControllerTrackingPlugin

Expand All @@ -30,23 +44,30 @@ class NavControllerActivityObserverTest {
@MockK
private lateinit var mockNavContextState: FlowState<Set<NavContext>>

private lateinit var mockAnalytics: Analytics

private lateinit var observer: NavControllerActivityObserver

@Before
fun setup() {
MockKAnnotations.init(this, relaxed = true)
Dispatchers.setMain(testDispatcher)

mockAnalytics = mockAnalytics(testScope, testDispatcher)

every { mockNavContext.callingActivity } returns mockActivity
every { mockActivity.lifecycle } returns mockk(relaxed = true)
every { mockActivity.lifecycle.addObserver(any()) } just Runs
every { mockPlugin.navContextState } returns mockNavContextState
every { mockPlugin.analytics } returns mockAnalytics
every { mockNavContextState.dispatch(any()) } just Runs

observer = NavControllerActivityObserver(mockPlugin, mockNavContext)
}

@After
fun tearDown() {
Dispatchers.resetMain()
unmockkAll()
}

Expand All @@ -61,10 +82,23 @@ class NavControllerActivityObserverTest {
}

@Test
fun `given observer is just created, then observer is also added as LifecycleObserver`() {
fun `given an observer, when addObserver called, then observer is added to activity lifecycle`() = runTest {
observer.addObserver()

advanceUntilIdle()

verify(exactly = 1) { observer.activityLifecycle()?.addObserver(observer) }
}

@Test
fun `given an observer, when removeObserver called, then observer is removed from activity lifecycle`() = runTest {
observer.removeObserver()

advanceUntilIdle()

verify(exactly = 1) { observer.activityLifecycle()?.removeObserver(observer) }
}

@Test
fun `given observer already created, when onStart called again, then OnDestinationChanged is not called again`() {
val mockDestination = mockk<NavDestination>(relaxed = true)
Expand All @@ -77,11 +111,10 @@ class NavControllerActivityObserverTest {
}

@Test
fun `given observer, when onDestroyed called, then observer is removed and RemoveNavContextAction dispatched`() {
fun `given observer, when onDestroyed called, then RemoveNavContextAction dispatched`() {
observer.onDestroy(mockActivity)

verify {
observer.activityLifecycle()?.removeObserver(observer)
mockNavContextState.dispatch(
match { it is NavContext.RemoveNavContextAction && it.navContext == mockNavContext }
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.rudderstack.android.sdk.plugins.screenrecording

import android.os.Bundle
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import com.rudderstack.android.sdk.state.NavContext
Expand All @@ -12,11 +11,10 @@ import com.rudderstack.kotlin.sdk.internals.statemanagement.FlowState
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.unmockkAll
import io.mockk.verify
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
Expand Down Expand Up @@ -76,30 +74,34 @@ class NavControllerTrackingPluginTest {
}

@Test
fun `when navContexts are added, then addOnDestinationChangedListener called and currentNavContexts updated`() =
fun `when navContexts are added, then addOnDestinationChangedListener and addObserver called for navContexts`() =
runTest {
val navContext1: NavContext = mockNavContext()
val navContext2: NavContext = mockNavContext()
val navContext1 = mockNavContext()
val navContext2 = mockNavContext()
mockkStatic(::provideNavControllerActivityObserver)
val observer1 = mockActivityObserverProvider(navContext1)
val observer2 = mockActivityObserverProvider(navContext2)

plugin.setup(mockAnalytics)
mockNavContextState.dispatch(NavContext.AddNavContextAction(navContext1))
mockNavContextState.dispatch(NavContext.AddNavContextAction(navContext2))
advanceUntilIdle()
assertEquals(2, plugin.currentNavContexts.size)
assertTrue(plugin.currentNavContexts.contains(navContext1))
assertTrue(plugin.currentNavContexts.contains(navContext2))

verify(exactly = 1) { navContext1.navController.addOnDestinationChangedListener(plugin) }
verify(exactly = 1) { navContext2.navController.addOnDestinationChangedListener(plugin) }

verify(exactly = 1) { observer1.addObserver() }
verify(exactly = 1) { observer2.addObserver() }
}

@Test
fun `when navControllers are removed, then removeOnDestinationChangedListener called and currentNavContexts updated`() =
fun `when navControllers are added and one of them is removed, then removeOnDestinationChangedListener and removeObserver called for that navContext`() =
runTest {
val navContext1 = mockNavContext()
val navContext2 = mockNavContext()

every { (navContext2.callingActivity as LifecycleOwner).lifecycle } returns mockk(relaxed = true)
every { (navContext2.callingActivity as LifecycleOwner).lifecycle.removeObserver(any()) } returns Unit
mockkStatic(::provideNavControllerActivityObserver)
val observer1 = mockActivityObserverProvider(navContext1)
val observer2 = mockActivityObserverProvider(navContext2)

plugin.setup(mockAnalytics)
mockNavContextState.dispatch(NavContext.AddNavContextAction(navContext1))
Expand All @@ -108,11 +110,11 @@ class NavControllerTrackingPluginTest {
mockNavContextState.dispatch(NavContext.RemoveNavContextAction(navContext2))
advanceUntilIdle()

// Verify that removeOnDestinationChangedListener was called for the removed navContext2 navController
assertEquals(1, plugin.currentNavContexts.size)
assertTrue(plugin.currentNavContexts.contains(navContext1))
verify(exactly = 1) { navContext2.navController.removeOnDestinationChangedListener(plugin) }
verify(exactly = 0) { navContext1.navController.removeOnDestinationChangedListener(plugin) }

verify(exactly = 1) { observer2.removeObserver() }
verify(exactly = 0) { observer1.removeObserver() }
}

@Test
Expand Down Expand Up @@ -164,4 +166,16 @@ class NavControllerTrackingPluginTest {

verify { mockAnalytics.screen(testRouteWithoutArgs, properties = automaticProperty()) }
}

private fun mockActivityObserverProvider(navContext: NavContext): NavControllerActivityObserver {
val mockObserver: NavControllerActivityObserver = mockk(relaxed = true)
every {
provideNavControllerActivityObserver(
plugin,
navContext
)
} returns mockObserver
every { mockObserver.find(navContext) } returns true
return mockObserver
}
}