diff --git a/apps/flipcash/shared/invite/build.gradle.kts b/apps/flipcash/shared/invite/build.gradle.kts index e5dd16d478..ab06609168 100644 --- a/apps/flipcash/shared/invite/build.gradle.kts +++ b/apps/flipcash/shared/invite/build.gradle.kts @@ -8,4 +8,9 @@ android { dependencies { implementation(project(":apps:flipcash:shared:shareable")) + + testImplementation(kotlin("test")) + testImplementation(libs.bundles.unit.testing) + testImplementation(libs.robolectric) + testImplementation(testFixtures(project(":libs:coroutines"))) } diff --git a/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/inject/InviteModule.kt b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/inject/InviteModule.kt index 4ddb603da0..05ab31a355 100644 --- a/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/inject/InviteModule.kt +++ b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/inject/InviteModule.kt @@ -4,6 +4,7 @@ import android.content.Context import com.flipcash.app.invite.InviteController import com.flipcash.app.invite.internal.InternalInviteController import com.flipcash.app.shareable.ShareSheetController +import com.flipcash.libs.coroutines.DispatcherProvider import com.getcode.util.resources.ResourceHelper import dagger.Module import dagger.Provides @@ -22,9 +23,11 @@ object InviteModule { @ApplicationContext context: Context, resources: ResourceHelper, shareController: ShareSheetController, + dispatchers: DispatcherProvider, ): InviteController = InternalInviteController( context = context, resources = resources, shareController = shareController, + dispatchers = dispatchers, ) } diff --git a/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/internal/InternalInviteController.kt b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/internal/InternalInviteController.kt index 620f4b9a4b..b1438996a4 100644 --- a/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/internal/InternalInviteController.kt +++ b/apps/flipcash/shared/invite/src/main/kotlin/com/flipcash/app/invite/internal/InternalInviteController.kt @@ -13,10 +13,14 @@ import com.flipcash.app.invite.InviteController import com.flipcash.app.shareable.ShareSheetController import com.flipcash.app.shareable.Shareable import com.flipcash.shared.invite.R +import com.flipcash.libs.coroutines.DispatcherProvider import com.getcode.util.resources.ResourceHelper import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking internal class InternalInviteController( @@ -24,13 +28,22 @@ internal class InternalInviteController( private val context: Context, private val resources: ResourceHelper, private val shareController: ShareSheetController, + dispatchers: DispatcherProvider, ) : InviteController { - private val _channels = MutableStateFlow(resolveChannels()) + // Hilt constructs this on the main thread when MainActivity is injected, and loading + // other apps' labels and icons reads their APKs, which ANRs on slow devices. + private val scope = CoroutineScope(SupervisorJob() + dispatchers.IO) + + private val _channels = MutableStateFlow>(emptyList()) override val channels: StateFlow> = _channels + init { + refresh() + } + override fun refresh() { - _channels.value = resolveChannels() + scope.launch { _channels.value = resolveChannels() } } private fun resolveChannels(): List = buildList { diff --git a/apps/flipcash/shared/invite/src/test/kotlin/com/flipcash/app/invite/internal/InternalInviteControllerTest.kt b/apps/flipcash/shared/invite/src/test/kotlin/com/flipcash/app/invite/internal/InternalInviteControllerTest.kt new file mode 100644 index 0000000000..15ad820bd9 --- /dev/null +++ b/apps/flipcash/shared/invite/src/test/kotlin/com/flipcash/app/invite/internal/InternalInviteControllerTest.kt @@ -0,0 +1,61 @@ +package com.flipcash.app.invite.internal + +import android.content.Context +import android.content.pm.PackageManager +import com.flipcash.app.invite.InviteChannelType +import com.flipcash.libs.coroutines.TestDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class InternalInviteControllerTest { + + private val packageManager: PackageManager = mockk { + every { queryIntentActivities(any(), any()) } returns emptyList() + every { getApplicationInfo(any(), any()) } throws PackageManager.NameNotFoundException() + } + private val context: Context = mockk { + every { packageManager } returns this@InternalInviteControllerTest.packageManager + } + + @Test + fun `construction does not query the package manager`() = runTest { + val dispatcher = StandardTestDispatcher(testScheduler) + + val controller = controller(dispatcher) + + assertTrue(controller.channels.value.isEmpty()) + verify(exactly = 0) { context.packageManager } + } + + @Test + fun `channels resolve on the io dispatcher`() = runTest { + val controller = controller(StandardTestDispatcher(testScheduler)) + + advanceUntilIdle() + + assertEquals( + listOf(InviteChannelType.Sms, InviteChannelType.More), + controller.channels.value.map { it.type }, + ) + } + + private fun controller(dispatcher: TestDispatcher) = InternalInviteController( + context = context, + resources = mockk(relaxed = true), + shareController = mockk(relaxed = true), + dispatchers = TestDispatcherProvider(dispatcher), + ) +}