From 5dca348a0d0c13a80b8b7ba073f1cb50ae4d9466 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 7 Aug 2026 15:28:45 +0530 Subject: [PATCH 1/9] Migrate ConnectJobIntroFragment to ConnectJobIntroViewModel --- .../viewmodel/ConnectJobIntroViewModel.kt | 34 +++++ .../connect/ConnectJobIntroFragment.kt | 123 ++++++++++-------- .../viewmodel/ConnectJobIntroViewModelTest.kt | 77 +++++++++++ .../connect/ConnectJobIntroFragmentTest.kt | 30 ++--- 4 files changed, 192 insertions(+), 72 deletions(-) create mode 100644 app/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModel.kt create mode 100644 app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt diff --git a/app/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModel.kt b/app/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModel.kt new file mode 100644 index 0000000000..e399825f4b --- /dev/null +++ b/app/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModel.kt @@ -0,0 +1,34 @@ +package org.commcare.connect.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.Job +import org.commcare.android.database.connect.models.ConnectUserRecord +import org.commcare.connect.repository.ConnectRepository +import org.commcare.connect.repository.DataState + +class ConnectJobIntroViewModel( + application: Application, + private val repository: ConnectRepository, +) : AndroidViewModel(application) { + constructor(application: Application) : this(application, ConnectRepository.getInstance(application)) + + private val _startLearning = MutableLiveData>() + val startLearning: LiveData> = _startLearning + + private var startLearningJob: Job? = null + + fun startLearning( + user: ConnectUserRecord, + jobUUID: String, + ) { + startLearningJob?.cancel() + startLearningJob = + collectInto( + flow = repository.startLearning(user, jobUUID), + liveData = _startLearning, + ) + } +} diff --git a/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt b/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt index 0c933cf9f8..f25106b10b 100644 --- a/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt +++ b/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt @@ -5,6 +5,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.NavHostFragment import org.commcare.AppUtils import org.commcare.android.database.connect.models.ConnectJobRecord @@ -14,8 +15,8 @@ import org.commcare.connect.ConnectMoneyUtils import org.commcare.connect.database.ConnectJobUtils import org.commcare.connect.database.ConnectUserDatabaseUtil import org.commcare.connect.network.PersonalIdOrConnectApiErrorHandler -import org.commcare.connect.network.base.BaseApiHandler.PersonalIdOrConnectApiErrorCodes -import org.commcare.connect.network.connect.ConnectApiHandler +import org.commcare.connect.repository.DataState +import org.commcare.connect.viewmodel.ConnectJobIntroViewModel import org.commcare.dalvik.R import org.commcare.dalvik.databinding.FragmentConnectJobIntroBinding import org.commcare.fragments.extensions.hasLiveView @@ -27,6 +28,8 @@ import java.text.DateFormat * */ class ConnectJobIntroFragment : ConnectJobFragment() { + private lateinit var viewModel: ConnectJobIntroViewModel + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -39,6 +42,14 @@ class ConnectJobIntroFragment : ConnectJobFragment + when (state) { + is DataState.Loading -> { + showLoading() + } + + is DataState.Success -> { + hideLoading() + reportApiCall(true) + + job.status = ConnectJobRecord.STATUS_LEARNING + ConnectJobUtils.upsertJob(job) + + if (!hasLiveView()) return@observe + hideError() + + val appId = job.learnAppInfo.appId + if (AppUtils.isAppInstalled(appId)) { + ConnectAppLaunchController(this) + .launchApp(appId, true, this::popSelfOnceHidden) + } else { + val title = getString(R.string.connect_downloading_learn) + NavHostFragment.findNavController(this).navigate( + ConnectJobIntroFragmentDirections + .actionConnectJobIntroFragmentToConnectDownloadingFragment(title, true), + ) + } + } + + is DataState.Error -> { + reportApiCall(false) + if (!hasLiveView()) return@observe + hideLoading() + val error = + PersonalIdOrConnectApiErrorHandler.handle( + requireActivity(), + state.errorCode, + state.throwable, + ) + if (PersonalIdOrConnectApiErrorHandler.isNetworkError(state.errorCode)) { + showError(getString(R.string.failed_to_start_learning)) + } else { + navigateToMessageDisplayDialog( + getString(R.string.error), + error, + false, + R.string.ok, + ) + } + } + + else -> {} + } + } + } + private fun populateLearnCard() { val modules = job.learnAppInfo.learnModules val totalHours = modules.sumOf { it.timeEstimate } @@ -102,56 +170,7 @@ class ConnectJobIntroFragment : ConnectJobFragment() { - override fun onFailure( - errorCode: PersonalIdOrConnectApiErrorCodes, - t: Throwable?, - ) { - reportApiCall(false) - if (!hasLiveView()) { - return - } - - val error = - PersonalIdOrConnectApiErrorHandler.handle(requireActivity(), errorCode, t) - if (PersonalIdOrConnectApiErrorHandler.isNetworkError(errorCode)) { - showError(getString(R.string.failed_to_start_learning)) - } else { - navigateToMessageDisplayDialog( - getString(R.string.error), - error, - false, - R.string.ok, - ) - } - } - - override fun onSuccess(success: Boolean) { - reportApiCall(success) - - job.status = ConnectJobRecord.STATUS_LEARNING - ConnectJobUtils.upsertJob(job) - - if (!hasLiveView()) { - return - } - - hideError() - - val appId = job.learnAppInfo.appId - if (AppUtils.isAppInstalled(appId)) { - ConnectAppLaunchController(this@ConnectJobIntroFragment) - .launchApp(appId, true, this@ConnectJobIntroFragment::popSelfOnceHidden) - } else { - val title = getString(R.string.connect_downloading_learn) - NavHostFragment.findNavController(this@ConnectJobIntroFragment).navigate( - ConnectJobIntroFragmentDirections - .actionConnectJobIntroFragmentToConnectDownloadingFragment(title, true), - ) - } - } - }.connectStartLearning(requireContext(), user, job.jobUUID) + viewModel.startLearning(user, job.jobUUID) } private fun reportApiCall(success: Boolean) { diff --git a/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt b/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt new file mode 100644 index 0000000000..5929713538 --- /dev/null +++ b/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt @@ -0,0 +1,77 @@ +package org.commcare.connect.viewmodel + +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import org.commcare.CommCareTestApplication +import org.commcare.connect.repository.ConnectRepository +import org.commcare.connect.repository.DataState +import org.commcare.rules.MainCoroutineRule +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@ExperimentalCoroutinesApi +@Config(application = CommCareTestApplication::class) +@RunWith(AndroidJUnit4::class) +class ConnectJobIntroViewModelTest { + @get:Rule + val mainCoroutineRule = MainCoroutineRule() + + private val application = ApplicationProvider.getApplicationContext() + private lateinit var mockRepository: ConnectRepository + private lateinit var viewModel: ConnectJobIntroViewModel + + @Before + fun setUp() { + mockRepository = mockk() + viewModel = ConnectJobIntroViewModel(application, mockRepository) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun testStartLearning_postsLoadingThenSuccess() { + every { mockRepository.startLearning(any(), any()) } returns + flowOf(DataState.Loading, DataState.Success(Unit)) + + val results = mutableListOf>() + viewModel.startLearning.observeForever { results.add(it) } + + mainCoroutineRule.runBlockingTest { + viewModel.startLearning(mockk(), "test-uuid") + } + + assertEquals(2, results.size) + assertEquals(DataState.Loading, results[0]) + assertTrue(results[1] is DataState.Success) + } + + @Test + fun testStartLearning_postsError_onFailure() { + every { mockRepository.startLearning(any(), any()) } returns + flowOf(DataState.Loading, DataState.Error()) + + val results = mutableListOf>() + viewModel.startLearning.observeForever { results.add(it) } + + mainCoroutineRule.runBlockingTest { + viewModel.startLearning(mockk(), "test-uuid") + } + + assertEquals(2, results.size) + assertTrue(results[1] is DataState.Error) + } +} diff --git a/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt b/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt index ed596c447b..6b41be0ad8 100644 --- a/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt +++ b/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt @@ -8,20 +8,19 @@ import io.mockk.every import io.mockk.mockkObject import io.mockk.mockkStatic import io.mockk.slot -import io.mockk.verify +import kotlinx.coroutines.flow.flow import org.commcare.AppUtils import org.commcare.CommCareTestApplication import org.commcare.android.database.connect.models.ConnectJobRecord import org.commcare.android.database.connect.models.ConnectUserRecord import org.commcare.android.database.connect.models.PersonalIdSessionData -import org.commcare.connect.ConnectAppUtils import org.commcare.connect.ConnectDateUtils import org.commcare.connect.ConnectMoneyUtils import org.commcare.connect.database.ConnectDatabaseHelper import org.commcare.connect.database.ConnectJobUtils import org.commcare.connect.database.ConnectUserDatabaseUtil -import org.commcare.connect.network.ApiConnect -import org.commcare.connect.network.IApiCallback +import org.commcare.connect.repository.ConnectRepository +import org.commcare.connect.repository.DataState import org.commcare.dalvik.R import org.commcare.views.connect.ConnectInfoCard import org.junit.Assert.assertEquals @@ -30,7 +29,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import org.robolectric.shadows.ShadowLooper -import java.io.ByteArrayInputStream import java.text.DateFormat import java.util.Date @@ -188,16 +186,13 @@ class ConnectJobIntroFragmentTest : BaseConnectJobIntroTest() { seedConnectUser() mockkStatic(AppUtils::class) every { AppUtils.isAppInstalled(any()) } returns false - mockkObject(ConnectAppUtils) - every { ConnectAppUtils.downloadApp(any(), any()) } returns Unit - // Capture the API callback without invoking it synchronously, so success is delivered - // after the click returns (as a real async network response would be). - val callbackSlot = slot() - mockkStatic(ApiConnect::class) - every { - ApiConnect.startLearnApp(any(), any(), any(), capture(callbackSlot)) - } returns Unit + val jobUuidSlot = slot() + val repo = ConnectRepository.getInstance(activity) + every { repo.startLearning(any(), capture(jobUuidSlot)) } returns + flow { + emit(DataState.Success(Unit)) + } val fragment = launch() val button = fragment.requireView().findViewById(R.id.cta_button) @@ -205,12 +200,7 @@ class ConnectJobIntroFragmentTest : BaseConnectJobIntroTest() { activity.runOnUiThread { button.performClick() } ShadowLooper.idleMainLooper() - activity.runOnUiThread { - callbackSlot.captured.processSuccess(200, ByteArrayInputStream(ByteArray(0))) - } - ShadowLooper.idleMainLooper() - - verify { ApiConnect.startLearnApp(any(), any(), eq(job.jobUUID), any()) } + assertEquals(job.jobUUID, jobUuidSlot.captured) assertEquals(ConnectJobRecord.STATUS_LEARNING, job.status) assertEquals( ConnectJobRecord.STATUS_LEARNING, From ed3ce5fb03010ca59b65fc60036310d8cdb852d8 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 7 Aug 2026 15:29:32 +0530 Subject: [PATCH 2/9] Migrate claim-job flow from ConnectJobClaimController into ConnectLearningProgressViewModel Co-Authored-By: Claude Sonnet 4.6 --- .../connect/ConnectJobClaimController.kt | 80 ------------------- .../connect/repository/ConnectRepository.kt | 34 +++++--- .../ConnectLearningProgressViewModel.kt | 17 ++++ .../ConnectLearningProgressFragment.java | 53 +++++++----- 4 files changed, 75 insertions(+), 109 deletions(-) delete mode 100644 app/src/org/commcare/connect/ConnectJobClaimController.kt diff --git a/app/src/org/commcare/connect/ConnectJobClaimController.kt b/app/src/org/commcare/connect/ConnectJobClaimController.kt deleted file mode 100644 index cce80a4d1c..0000000000 --- a/app/src/org/commcare/connect/ConnectJobClaimController.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.commcare.connect - -import android.content.Context -import org.commcare.AppUtils -import org.commcare.android.database.connect.models.ConnectJobRecord -import org.commcare.connect.database.ConnectJobUtils -import org.commcare.connect.database.ConnectUserDatabaseUtil -import org.commcare.connect.network.PersonalIdOrConnectApiErrorHandler -import org.commcare.connect.network.base.BaseApiHandler.PersonalIdOrConnectApiErrorCodes -import org.commcare.connect.network.connect.ConnectApiHandler -import org.commcare.dalvik.R -import org.commcare.google.services.analytics.FirebaseAnalyticsUtil - -/** - * Claims an opportunity so the worker can start delivering, skipping the server call if the job is - * already claimed. - * - * Callers navigate from [OnClaimedListener], which reports whether the delivery app is installed. - */ -class ConnectJobClaimController( - private val context: Context, -) { - fun interface OnClaimedListener { - fun onClaimed(deliveryAppInstalled: Boolean) - } - - fun interface OnErrorListener { - fun onError(message: String) - } - - fun claimIfNeededAndProceed( - job: ConnectJobRecord, - onClaimed: OnClaimedListener, - onError: OnErrorListener, - ) { - val deliveryAppInstalled = AppUtils.isAppInstalled(job.deliveryAppInfo.appId) - - if (job.status == ConnectJobRecord.STATUS_DELIVERING) { - proceed(job, deliveryAppInstalled, onClaimed) - return - } - - val user = ConnectUserDatabaseUtil.getUser(context) - - object : ConnectApiHandler() { - override fun onSuccess(success: Boolean) { - FirebaseAnalyticsUtil.reportCccApiClaimJob(true) - proceed(job, deliveryAppInstalled, onClaimed) - } - - override fun onFailure( - errorCode: PersonalIdOrConnectApiErrorCodes, - t: Throwable?, - ) { - FirebaseAnalyticsUtil.reportCccApiClaimJob(false) - onError.onError(errorMessage(errorCode, t)) - } - }.claimJob(context, user, job.jobUUID) - } - - private fun proceed( - job: ConnectJobRecord, - deliveryAppInstalled: Boolean, - onClaimed: OnClaimedListener, - ) { - job.status = ConnectJobRecord.STATUS_DELIVERING - ConnectJobUtils.upsertJob(job) - onClaimed.onClaimed(deliveryAppInstalled) - } - - private fun errorMessage( - errorCode: PersonalIdOrConnectApiErrorCodes, - t: Throwable?, - ): String = - if (errorCode == PersonalIdOrConnectApiErrorCodes.BAD_REQUEST_ERROR) { - context.getString(R.string.recovery_unable_to_claim_opportunity) - } else { - PersonalIdOrConnectApiErrorHandler.handle(context, errorCode, t) - } -} diff --git a/app/src/org/commcare/connect/repository/ConnectRepository.kt b/app/src/org/commcare/connect/repository/ConnectRepository.kt index 5e5b768aaf..2ee571c571 100644 --- a/app/src/org/commcare/connect/repository/ConnectRepository.kt +++ b/app/src/org/commcare/connect/repository/ConnectRepository.kt @@ -2,13 +2,13 @@ package org.commcare.connect.repository import android.content.Context import androidx.annotation.VisibleForTesting -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import org.commcare.CommCareApplication import org.commcare.android.database.connect.models.ConnectJobRecord import org.commcare.android.database.connect.models.ConnectUserRecord +import org.commcare.connect.database.ConnectJobUtils import org.commcare.connect.database.ConnectJobUtils.getCompositeJob import org.commcare.connect.database.ConnectJobUtils.getCompositeJobs import org.commcare.connect.database.ConnectUserDatabaseUtil @@ -17,6 +17,7 @@ import org.commcare.connect.network.connect.models.ConnectPaymentConfirmationMod import org.commcare.connect.network.connect.models.DeliveryAppProgressResponseModel import org.commcare.connect.network.connect.models.LearningAppProgressResponseModel import org.commcare.connect.network.connect.models.applyToJob +import org.commcare.utils.coroutines.DispatcherProvider class ConnectRepository @VisibleForTesting @@ -96,12 +97,22 @@ class ConnectRepository ) fun startLearning(jobUUID: String): Flow> = - networkOnlyFlow { networkClient.startLearnApp(getConnectUser(), jobUUID) } + networkOnlyFlow(networkCall = { networkClient.startLearnApp(getConnectUser(), jobUUID) }) - fun claimJob(jobUUID: String): Flow> = networkOnlyFlow { networkClient.claimJob(getConnectUser(), jobUUID) } + fun claimJob(job: ConnectJobRecord): Flow> = + networkOnlyFlow( + networkCall = { + val user = ConnectUserDatabaseUtil.getUser(CommCareApplication.instance()) + networkClient.claimJob(user, job.jobUUID) + }, + onNetworkSuccess = { + job.status = ConnectJobRecord.STATUS_DELIVERING + ConnectJobUtils.upsertJob(job) + }, + ) fun confirmPayments(paymentConfirmations: List): Flow> = - networkOnlyFlow { networkClient.confirmPayments(getConnectUser(), paymentConfirmations) } + networkOnlyFlow(networkCall = { networkClient.confirmPayments(getConnectUser(), paymentConfirmations) }) /** * Emits Cached first,then Loading, then Success or Error after network call. @@ -139,15 +150,20 @@ class ConnectRepository result .onSuccess { data -> emit(DataState.Success(mapToEmit(data))) } .onFailure { throwable -> emit(DataState.Error.from(throwable)) } - }.flowOn(Dispatchers.IO) + }.flowOn(DispatcherProvider.io()) - private fun networkOnlyFlow(networkCall: suspend () -> Result): Flow> = + private fun networkOnlyFlow( + networkCall: suspend () -> Result, + onNetworkSuccess: suspend (T) -> Unit = {}, + ): Flow> = flow { emit(DataState.Loading) networkCall() - .onSuccess { emit(DataState.Success(it)) } - .onFailure { emit(DataState.Error.from(it)) } - }.flowOn(Dispatchers.IO) + .onSuccess { data -> + onNetworkSuccess(data) + emit(DataState.Success(data)) + }.onFailure { emit(DataState.Error.from(it)) } + }.flowOn(DispatcherProvider.io()) private fun getConnectUser(): ConnectUserRecord = requireNotNull(ConnectUserDatabaseUtil.getUser(CommCareApplication.instance())) { "No Connect user found" } diff --git a/app/src/org/commcare/connect/viewmodel/ConnectLearningProgressViewModel.kt b/app/src/org/commcare/connect/viewmodel/ConnectLearningProgressViewModel.kt index e786754248..a911e406b5 100644 --- a/app/src/org/commcare/connect/viewmodel/ConnectLearningProgressViewModel.kt +++ b/app/src/org/commcare/connect/viewmodel/ConnectLearningProgressViewModel.kt @@ -20,7 +20,11 @@ class ConnectLearningProgressViewModel( private val _learningProgress = MutableLiveData>() val learningProgress: LiveData> = _learningProgress + private val _claimJob = MutableLiveData>() + val claimJob: LiveData> = _claimJob + private var loadLearnProgressJob: Job? = null + private var claimJobCoroutine: Job? = null fun loadLearningProgress( opportunity: ConnectJobRecord, @@ -33,4 +37,17 @@ class ConnectLearningProgressViewModel( liveData = _learningProgress, ) } + + fun claimJob(job: ConnectJobRecord) { + if (job.status == ConnectJobRecord.STATUS_DELIVERING) { + _claimJob.value = DataState.Success(Unit) + return + } + claimJobCoroutine?.cancel() + claimJobCoroutine = + collectInto( + flow = repository.claimJob(job), + liveData = _claimJob, + ) + } } diff --git a/app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java b/app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java index a5c4459fa5..7a5636f015 100644 --- a/app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java +++ b/app/src/org/commcare/fragments/connect/ConnectLearningProgressFragment.java @@ -13,10 +13,13 @@ import org.commcare.android.database.connect.models.ConnectJobAssessmentRecord; import org.commcare.android.database.connect.models.ConnectJobLearningRecord; import org.commcare.connect.ConnectAppLaunchController; -import org.commcare.connect.ConnectJobClaimController; import org.commcare.connect.PersonalIdManager; import org.commcare.connect.database.ConnectUserDatabaseUtil; +import org.commcare.connect.network.PersonalIdOrConnectApiErrorHandler; +import org.commcare.connect.network.base.BaseApiHandler.PersonalIdOrConnectApiErrorCodes; +import org.commcare.google.services.analytics.FirebaseAnalyticsUtil; import org.commcare.connect.repository.ConnectRepository; +import org.commcare.connect.repository.DataState; import org.commcare.connect.viewmodel.ConnectLearningProgressViewModel; import org.commcare.dalvik.R; import org.commcare.dalvik.databinding.FragmentConnectLearningProgressBinding; @@ -68,6 +71,7 @@ public static ConnectLearningProgressFragment newInstance(boolean showAppLaunch) populateJobCard(); updateLearningUI(); observeLearningProgress(); + observeClaimJob(); return view; } @@ -134,27 +138,36 @@ private void updateProgressViews(int learningProgressPercent) { private void onDeliveryCtaClicked(View view) { getBinding().learnCompleteView.hideClaimFailure(); getBinding().learnCompleteView.setCtaEnabled(false); + viewModel.claimJob(job); + } - new ConnectJobClaimController(requireContext()).claimIfNeededAndProceed( - job, - deliveryAppInstalled -> { - if (!hasLiveView(this)) { - return; - } - Navigation.findNavController(view).navigate( - deliveryAppInstalled - ? navigateToDeliveryProgress() - : navigateToDeliveryDownload() - ); - }, - message -> { - if (!hasLiveView(this)) { - return; - } - getBinding().learnCompleteView.setCtaEnabled(true); - getBinding().learnCompleteView.showClaimFailure(message); + private void observeClaimJob() { + viewModel.getClaimJob().observe(getViewLifecycleOwner(), state -> { + if (state instanceof DataState.Success) { + FirebaseAnalyticsUtil.reportCccApiClaimJob(true); + if (!hasLiveView(this)) { + return; } - ); + boolean deliveryAppInstalled = AppUtils.isAppInstalled(job.getDeliveryAppInfo().getAppId()); + Navigation.findNavController(requireView()).navigate( + deliveryAppInstalled + ? navigateToDeliveryProgress() + : navigateToDeliveryDownload() + ); + } else if (state instanceof DataState.Error) { + FirebaseAnalyticsUtil.reportCccApiClaimJob(false); + DataState.Error error = (DataState.Error) state; + if (!hasLiveView(this)) { + return; + } + getBinding().learnCompleteView.setCtaEnabled(true); + PersonalIdOrConnectApiErrorCodes errorCode = error.getErrorCode(); + String message = errorCode == PersonalIdOrConnectApiErrorCodes.BAD_REQUEST_ERROR + ? getString(R.string.recovery_unable_to_claim_opportunity) + : PersonalIdOrConnectApiErrorHandler.handle(requireContext(), errorCode, error.getThrowable()); + getBinding().learnCompleteView.showClaimFailure(message); + } + }); } private NavDirections navigateToDeliveryProgress() { From 42456628cd0c4252c2b257538c609aaeee28552f Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 14 Aug 2026 19:00:24 +0530 Subject: [PATCH 3/9] Add network access methods to ConnectRepository Introduce getOpportunities, getLearningProgress, getDeliveryProgress, startLearning, claimJob, confirmPayments, and syncJobProgress on ConnectRepository, backed by the offline-first and network-only flow primitives. ConnectRepository is now the single network access point for all Connect features. Co-Authored-By: Claude Sonnet 4.6 --- .../connect/repository/ConnectRepository.kt | 143 +++++++++++++++++- 1 file changed, 138 insertions(+), 5 deletions(-) diff --git a/app/src/org/commcare/connect/repository/ConnectRepository.kt b/app/src/org/commcare/connect/repository/ConnectRepository.kt index 2ee571c571..2857eff2c8 100644 --- a/app/src/org/commcare/connect/repository/ConnectRepository.kt +++ b/app/src/org/commcare/connect/repository/ConnectRepository.kt @@ -2,12 +2,16 @@ package org.commcare.connect.repository import android.content.Context import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.commcare.CommCareApplication import org.commcare.android.database.connect.models.ConnectJobRecord import org.commcare.android.database.connect.models.ConnectUserRecord +import org.commcare.connect.ConnectActivityCompleteListener import org.commcare.connect.database.ConnectJobUtils import org.commcare.connect.database.ConnectJobUtils.getCompositeJob import org.commcare.connect.database.ConnectJobUtils.getCompositeJobs @@ -17,6 +21,10 @@ import org.commcare.connect.network.connect.models.ConnectPaymentConfirmationMod import org.commcare.connect.network.connect.models.DeliveryAppProgressResponseModel import org.commcare.connect.network.connect.models.LearningAppProgressResponseModel import org.commcare.connect.network.connect.models.applyToJob +import org.commcare.google.services.analytics.AnalyticsParamValue.FINISH_DELIVERY +import org.commcare.google.services.analytics.AnalyticsParamValue.PAID_DELIVERY +import org.commcare.google.services.analytics.AnalyticsParamValue.START_DELIVERY +import org.commcare.google.services.analytics.FirebaseAnalyticsUtil import org.commcare.utils.coroutines.DispatcherProvider class ConnectRepository @@ -33,6 +41,7 @@ class ConnectRepository @Volatile private var instance: ConnectRepository? = null + @JvmStatic fun getInstance(context: Context): ConnectRepository = instance ?: synchronized(this) { instance ?: ConnectRepository( @@ -75,7 +84,11 @@ class ConnectRepository networkCall = { fetchLearningProgressFromNetwork(job) }, onNetworkSuccess = { responseModel -> responseModel.applyToJob(job, CommCareApplication.instance()) + if (job.passedAssessment()) { + FirebaseAnalyticsUtil.reportCccApiLearnProgress(true) + } }, + onNetworkFailure = { FirebaseAnalyticsUtil.reportCccApiLearnProgress(false) }, mapToEmit = { _ -> getCompositeJob(CommCareApplication.instance(), job.jobUUID) }, ) @@ -91,13 +104,21 @@ class ConnectRepository loadCache = { getCompositeJob(CommCareApplication.instance(), job.jobUUID) }, networkCall = { fetchDeliveryProgressFromNetwork(job) }, onNetworkSuccess = { responseModel -> + val events = mutableSetOf() + if (responseModel.updatedJob) events.add(START_DELIVERY) + if (responseModel.hasDeliveries && job.getDeliveryProgressPercentage() == 100) events.add(FINISH_DELIVERY) + if (responseModel.hasPayment && job.payments.isNotEmpty()) events.add(PAID_DELIVERY) responseModel.applyToJob(job, CommCareApplication.instance()) + events.forEach { event -> FirebaseAnalyticsUtil.reportCccApiDeliveryProgress(true, event) } }, + onNetworkFailure = { FirebaseAnalyticsUtil.reportCccApiDeliveryProgress(false, null) }, mapToEmit = { _ -> getCompositeJob(CommCareApplication.instance(), job.jobUUID) }, ) - fun startLearning(jobUUID: String): Flow> = - networkOnlyFlow(networkCall = { networkClient.startLearnApp(getConnectUser(), jobUUID) }) + fun startLearning( + user: ConnectUserRecord, + jobUUID: String, + ): Flow> = networkOnlyFlow(networkCall = { networkClient.startLearnApp(user, jobUUID) }) fun claimJob(job: ConnectJobRecord): Flow> = networkOnlyFlow( @@ -112,7 +133,25 @@ class ConnectRepository ) fun confirmPayments(paymentConfirmations: List): Flow> = - networkOnlyFlow(networkCall = { networkClient.confirmPayments(getConnectUser(), paymentConfirmations) }) + networkOnlyFlow( + networkCall = { networkClient.confirmPayments(getConnectUser(), paymentConfirmations) }, + onNetworkSuccess = { + for (paymentConfirmation in paymentConfirmations) { + paymentConfirmation.payment.confirmed = paymentConfirmation.toConfirm + ConnectJobUtils.storePayment(CommCareApplication.instance(), paymentConfirmation.payment) + } + FirebaseAnalyticsUtil.reportCccApiPaymentConfirmation(true) + }, + onNetworkFailure = { FirebaseAnalyticsUtil.reportCccApiPaymentConfirmation(false) }, + ) + + fun syncJobProgress(job: ConnectJobRecord): Flow> = + when (job.status) { + ConnectJobRecord.STATUS_LEARNING -> getLearningProgress(job) + ConnectJobRecord.STATUS_DELIVERING -> getDeliveryProgress(job) + else -> flow { emit(DataState.Success(job)) } + } + /** * Emits Cached first,then Loading, then Success or Error after network call. @@ -125,6 +164,7 @@ class ConnectRepository loadCache: () -> C?, networkCall: suspend () -> Result, onNetworkSuccess: suspend (N) -> Unit, + onNetworkFailure: suspend (Throwable) -> Unit = {}, mapToEmit: suspend (N) -> C, ): Flow> = flow { @@ -149,12 +189,16 @@ class ConnectRepository } result .onSuccess { data -> emit(DataState.Success(mapToEmit(data))) } - .onFailure { throwable -> emit(DataState.Error.from(throwable)) } + .onFailure { throwable -> + onNetworkFailure(throwable) + emit(DataState.Error.from(throwable)) + } }.flowOn(DispatcherProvider.io()) private fun networkOnlyFlow( networkCall: suspend () -> Result, onNetworkSuccess: suspend (T) -> Unit = {}, + onNetworkFailure: suspend (Throwable) -> Unit = {}, ): Flow> = flow { emit(DataState.Loading) @@ -162,7 +206,10 @@ class ConnectRepository .onSuccess { data -> onNetworkSuccess(data) emit(DataState.Success(data)) - }.onFailure { emit(DataState.Error.from(it)) } + }.onFailure { + onNetworkFailure(it) + emit(DataState.Error.from(it)) + } }.flowOn(DispatcherProvider.io()) private fun getConnectUser(): ConnectUserRecord = @@ -176,4 +223,90 @@ class ConnectRepository private suspend fun fetchDeliveryProgressFromNetwork(job: ConnectJobRecord): Result = networkClient.getDeliveryProgress(getConnectUser(), job) + + // Java interop — use the Flow-returning equivalents from Kotlin. + + fun retrieveOpportunitiesForJava(listener: ConnectActivityCompleteListener) { + CoroutineScope(DispatcherProvider.io()).launch { + getOpportunities(forceRefresh = true).collect { state -> + when (state) { + is DataState.Success -> { + withContext(DispatcherProvider.main()) { + listener.connectActivityComplete( + true + ) + } + } + + is DataState.Error -> { + withContext(DispatcherProvider.main()) { + listener.connectActivityComplete( + false + ) + } + } + + else -> {} + } + } + } + } + + fun updateDeliveryProgressForJava( + job: ConnectJobRecord, + listener: ConnectActivityCompleteListener, + ) { + CoroutineScope(DispatcherProvider.io()).launch { + getDeliveryProgress(job).collect { state -> + when (state) { + is DataState.Success -> { + withContext(DispatcherProvider.main()) { + listener.connectActivityComplete( + true + ) + } + } + + is DataState.Error -> { + withContext(DispatcherProvider.main()) { + listener.connectActivityComplete( + false + ) + } + } + + else -> {} + } + } + } + } + + fun updatePaymentsConfirmedForJava( + paymentConfirmations: List, + listener: ConnectActivityCompleteListener, + ) { + CoroutineScope(DispatcherProvider.io()).launch { + confirmPayments(paymentConfirmations).collect { state -> + when (state) { + is DataState.Success -> { + withContext(DispatcherProvider.main()) { + listener.connectActivityComplete( + true + ) + } + } + + is DataState.Error -> { + withContext(DispatcherProvider.main()) { + listener.connectActivityComplete( + false + ) + } + } + + else -> {} + } + } + } + } } From 5698d6f86669ac58efb6ecb3592980e11aea057b Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 14 Aug 2026 19:00:32 +0530 Subject: [PATCH 4/9] Migrate all Connect network call sites from ConnectApiHandler/ConnectJobHelper to ConnectRepository Replace every ConnectApiHandler inline usage and ConnectJobHelper network method with the equivalent ConnectRepository flow or callback. Callers in fragments and activities now go through ConnectRepository directly; ConnectJobHelper retains only its non-network helpers. Co-Authored-By: Claude Sonnet 4.6 --- .../activities/CommCareSetupActivity.java | 47 +++--- .../activities/StandardHomeActivity.java | 3 +- .../org/commcare/connect/ConnectJobHelper.kt | 150 ------------------ .../connect/repository/ConnectRepository.kt | 128 ++++++--------- .../viewmodel/ConnectJobIntroViewModel.kt | 8 +- .../ConnectDeliveryPaymentFragment.java | 7 +- .../ConnectDeliveryProgressFragment.java | 4 +- .../connect/ConnectJobIntroFragment.kt | 4 +- .../connect/ConnectUnlockFragment.java | 37 ++--- .../commcare/login/PostLoginSideEffects.kt | 17 +- .../pn/workers/NotificationsSyncWorker.kt | 73 +++++---- .../viewmodel/ConnectJobIntroViewModelTest.kt | 8 +- .../connect/ConnectJobIntroFragmentTest.kt | 2 +- 13 files changed, 132 insertions(+), 356 deletions(-) diff --git a/app/src/org/commcare/activities/CommCareSetupActivity.java b/app/src/org/commcare/activities/CommCareSetupActivity.java index 4a66ca01e3..1303d6ca7c 100644 --- a/app/src/org/commcare/activities/CommCareSetupActivity.java +++ b/app/src/org/commcare/activities/CommCareSetupActivity.java @@ -16,14 +16,13 @@ import org.commcare.CommCareApp; import org.commcare.CommCareApplication; import org.commcare.android.database.connect.models.ConnectJobRecord; -import org.commcare.android.database.connect.models.ConnectUserRecord; import org.commcare.connect.ConnectConstants; +import org.commcare.connect.repository.ConnectRepository; import org.commcare.connect.ConnectNavHelper; import org.commcare.connect.PersonalIdManager; import org.commcare.personalId.UnlockPolicy; +import org.commcare.connect.database.ConnectJobUtils; import org.commcare.connect.database.ConnectUserDatabaseUtil; -import org.commcare.connect.network.PersonalIdOrConnectApiErrorHandler; -import org.commcare.connect.network.connect.ConnectApiHandler; import org.commcare.dalvik.BuildConfig; import org.commcare.dalvik.R; import org.commcare.engine.resource.AppInstallStatus; @@ -999,30 +998,24 @@ public void checkManagedConfiguration() { } private void refreshOpportunities() { - CommCareActivity activity = this; - ConnectUserRecord user = ConnectUserDatabaseUtil.getUser(activity); - new ConnectApiHandler>() { - - @Override - public void onFailure(@NonNull PersonalIdOrConnectApiErrorCodes errorCode, @Nullable Throwable t) { - String error = PersonalIdOrConnectApiErrorHandler.handle(activity, errorCode, t); - Toast.makeText(activity, error, Toast.LENGTH_LONG).show(); - } - - @Override - public void onSuccess(List jobs) { - boolean connectAccess = !jobs.isEmpty(); - String toastMessage = getString(R.string.setup_refresh_opportunities_no_jobs); - if (connectAccess) { - ConnectUserDatabaseUtil.turnOnConnectAccess(activity); - - updateConnectButton(); - refreshDrawer(); - - toastMessage = getString(R.string.setup_refresh_opportunities_with_jobs); + ConnectRepository.getInstance(this).retrieveOpportunitiesForJava( + (success, error) -> { + if (success) { + boolean connectAccess = !ConnectJobUtils.getCompositeJobs( + this, ConnectJobRecord.STATUS_ALL_JOBS, null).isEmpty(); + if (connectAccess) { + ConnectUserDatabaseUtil.turnOnConnectAccess(this); + updateConnectButton(); + refreshDrawer(); + } + String toastMessage = connectAccess + ? getString(R.string.setup_refresh_opportunities_with_jobs) + : getString(R.string.setup_refresh_opportunities_no_jobs); + Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(this, error, Toast.LENGTH_LONG).show(); + } } - Toast.makeText(activity, toastMessage, Toast.LENGTH_LONG).show(); - } - }.getConnectOpportunities(activity, user); + ); } } diff --git a/app/src/org/commcare/activities/StandardHomeActivity.java b/app/src/org/commcare/activities/StandardHomeActivity.java index e7c05ba61b..6cd3c42fc5 100755 --- a/app/src/org/commcare/activities/StandardHomeActivity.java +++ b/app/src/org/commcare/activities/StandardHomeActivity.java @@ -13,6 +13,7 @@ import org.commcare.CommCareNoficationManager; import org.commcare.connect.ConnectJobHelper; import org.commcare.android.database.connect.models.ConnectJobRecord; +import org.commcare.connect.repository.ConnectRepository; import org.commcare.connect.ConnectNavHelper; import org.commcare.connect.EmailOfferHelper; import org.commcare.dalvik.R; @@ -369,7 +370,7 @@ void refreshCCUpdateOption() { public void fetchJobProgressOverNetwork() { ConnectJobRecord job = getActiveJob(); if(job != null && job.getStatus() == ConnectJobRecord.STATUS_DELIVERING) { - ConnectJobHelper.INSTANCE.updateDeliveryProgress(this, job, (success, error) -> { + ConnectRepository.getInstance(this).updateDeliveryProgressForJava(job, (success, error) -> { if (success) { uiController.updateConnectJobProgress(); } diff --git a/app/src/org/commcare/connect/ConnectJobHelper.kt b/app/src/org/commcare/connect/ConnectJobHelper.kt index d391f15a81..149a55ae45 100644 --- a/app/src/org/commcare/connect/ConnectJobHelper.kt +++ b/app/src/org/commcare/connect/ConnectJobHelper.kt @@ -1,23 +1,9 @@ package org.commcare.connect import android.content.Context -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.commcare.CommCareApplication import org.commcare.android.database.connect.models.ConnectJobRecord import org.commcare.connect.database.ConnectJobUtils -import org.commcare.connect.database.ConnectUserDatabaseUtil -import org.commcare.connect.network.PersonalIdOrConnectApiErrorHandler -import org.commcare.connect.network.connect.ConnectApiHandler -import org.commcare.connect.network.connect.ConnectNetworkClient -import org.commcare.connect.network.connect.models.ConnectPaymentConfirmationModel -import org.commcare.connect.network.connect.models.applyToJob -import org.commcare.google.services.analytics.AnalyticsParamValue.FINISH_DELIVERY -import org.commcare.google.services.analytics.AnalyticsParamValue.PAID_DELIVERY -import org.commcare.google.services.analytics.AnalyticsParamValue.START_DELIVERY -import org.commcare.google.services.analytics.FirebaseAnalyticsUtil object ConnectJobHelper { fun getJobForSeatedApp(context: Context): ConnectJobRecord? { @@ -38,142 +24,6 @@ object ConnectJobHelper { return !record.isLearning || job.status != ConnectJobRecord.STATUS_DELIVERING } - fun updateJobProgress( - context: Context, - job: ConnectJobRecord, - listener: ConnectActivityCompleteListener, - ) { - when (job.status) { - ConnectJobRecord.STATUS_LEARNING -> { - updateLearningProgress(context, job, listener) - } - - ConnectJobRecord.STATUS_DELIVERING -> { - updateDeliveryProgress(context, job, listener) - } - - else -> { - listener.connectActivityComplete(true) - } - } - } - - fun updateLearningProgress( - context: Context, - job: ConnectJobRecord, - listener: ConnectActivityCompleteListener, - ) { - val user = ConnectUserDatabaseUtil.getUser(context)!! - CoroutineScope(Dispatchers.IO).launch { - val result = ConnectNetworkClient.getInstance().getLearningProgress(user, job) - result.fold( - onSuccess = { model -> - model.applyToJob(job, context) - if (job.passedAssessment()) { - FirebaseAnalyticsUtil.reportCccApiLearnProgress(true) - } - withContext(Dispatchers.Main) { - listener.connectActivityComplete(true) - } - }, - onFailure = { - FirebaseAnalyticsUtil.reportCccApiLearnProgress(false) - withContext(Dispatchers.Main) { - listener.connectActivityComplete(false) - } - }, - ) - } - } - - fun updateDeliveryProgress( - context: Context, - job: ConnectJobRecord, - listener: ConnectActivityCompleteListener, - ) { - val user = ConnectUserDatabaseUtil.getUser(context)!! - CoroutineScope(Dispatchers.IO).launch { - val result = ConnectNetworkClient.getInstance().getDeliveryProgress(user, job) - result.fold( - onSuccess = { model -> - val events = mutableSetOf() - if (model.updatedJob) { - events.add(START_DELIVERY) - } - if (model.hasDeliveries && job.getDeliveryProgressPercentage() == 100) { - events.add(FINISH_DELIVERY) - } - if (model.hasPayment && job.payments.isNotEmpty()) { - events.add(PAID_DELIVERY) - } - - model.applyToJob(job, context) - - events.forEach { event -> - FirebaseAnalyticsUtil.reportCccApiDeliveryProgress(true, event) - } - withContext(Dispatchers.Main) { - listener.connectActivityComplete(true) - } - }, - onFailure = { - FirebaseAnalyticsUtil.reportCccApiDeliveryProgress(false, null) - withContext(Dispatchers.Main) { - listener.connectActivityComplete(false) - } - }, - ) - } - } - - fun updatePaymentsConfirmed( - context: Context, - paymentConfirmations: List, - listener: ConnectActivityCompleteListener, - ) { - val user = ConnectUserDatabaseUtil.getUser(context) - - object : ConnectApiHandler() { - override fun onSuccess(data: Boolean) { - for (paymentConfirmation in paymentConfirmations) { - paymentConfirmation.payment.confirmed = paymentConfirmation.toConfirm - ConnectJobUtils.storePayment(context, paymentConfirmation.payment) - } - - FirebaseAnalyticsUtil.reportCccApiPaymentConfirmation(true) - listener.connectActivityComplete(true) - } - - override fun onFailure( - errorCode: PersonalIdOrConnectApiErrorCodes, - t: Throwable?, - ) { - val error = PersonalIdOrConnectApiErrorHandler.handle(context, errorCode, t) - FirebaseAnalyticsUtil.reportCccApiPaymentConfirmation(false) - listener.connectActivityComplete(false, error) - } - }.setPaymentConfirmations(context, user, paymentConfirmations) - } - - fun retrieveOpportunities( - context: Context, - listener: ConnectActivityCompleteListener, - ) { - val user = ConnectUserDatabaseUtil.getUser(context) - object : ConnectApiHandler>() { - override fun onFailure( - errorCode: PersonalIdOrConnectApiErrorCodes, - t: Throwable?, - ) { - listener.connectActivityComplete(false) - } - - override fun onSuccess(data: List) { - listener.connectActivityComplete(true) - } - }.getConnectOpportunities(context, user!!) - } - fun resolveGenericOpportunityDestination( currentAction: String?, job: ConnectJobRecord?, diff --git a/app/src/org/commcare/connect/repository/ConnectRepository.kt b/app/src/org/commcare/connect/repository/ConnectRepository.kt index 2857eff2c8..8d2f9f2429 100644 --- a/app/src/org/commcare/connect/repository/ConnectRepository.kt +++ b/app/src/org/commcare/connect/repository/ConnectRepository.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.annotation.VisibleForTesting import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch @@ -16,6 +17,7 @@ import org.commcare.connect.database.ConnectJobUtils import org.commcare.connect.database.ConnectJobUtils.getCompositeJob import org.commcare.connect.database.ConnectJobUtils.getCompositeJobs import org.commcare.connect.database.ConnectUserDatabaseUtil +import org.commcare.connect.network.PersonalIdOrConnectApiErrorHandler import org.commcare.connect.network.connect.ConnectNetworkClient import org.commcare.connect.network.connect.models.ConnectPaymentConfirmationModel import org.commcare.connect.network.connect.models.DeliveryAppProgressResponseModel @@ -115,17 +117,12 @@ class ConnectRepository mapToEmit = { _ -> getCompositeJob(CommCareApplication.instance(), job.jobUUID) }, ) - fun startLearning( - user: ConnectUserRecord, - jobUUID: String, - ): Flow> = networkOnlyFlow(networkCall = { networkClient.startLearnApp(user, jobUUID) }) + fun startLearning(jobUUID: String): Flow> = + networkOnlyFlow(networkCall = { networkClient.startLearnApp(getConnectUser(), jobUUID) }) fun claimJob(job: ConnectJobRecord): Flow> = networkOnlyFlow( - networkCall = { - val user = ConnectUserDatabaseUtil.getUser(CommCareApplication.instance()) - networkClient.claimJob(user, job.jobUUID) - }, + networkCall = { networkClient.claimJob(getConnectUser(), job.jobUUID) }, onNetworkSuccess = { job.status = ConnectJobRecord.STATUS_DELIVERING ConnectJobUtils.upsertJob(job) @@ -152,10 +149,12 @@ class ConnectRepository else -> flow { emit(DataState.Success(job)) } } - /** * Emits Cached first,then Loading, then Success or Error after network call. * DB writes go in [onNetworkSuccess], re-read in [mapToEmit]. + * + * Used for GET requests that have a cached value to emit first, then make a network call to update the cache and emit the updated value. + * Uses ConnectRequestManager to deduplicate requests for the same syncKey. */ private fun offlineFirstFlow( syncKey: String, @@ -195,6 +194,13 @@ class ConnectRepository } }.flowOn(DispatcherProvider.io()) + /** + * Emits Loading, then Success or Error after network call. + * No cached emission, always make requests to network unlike [offlineFirstFlow]. + * Doesn't use ConnectRequestManager to deduplicate requests. + * + * Used for one-time actions, mostly POST requests, that don't have a cached value to emit first. + */ private fun networkOnlyFlow( networkCall: suspend () -> Result, onNetworkSuccess: suspend (T) -> Unit = {}, @@ -224,89 +230,47 @@ class ConnectRepository private suspend fun fetchDeliveryProgressFromNetwork(job: ConnectJobRecord): Result = networkClient.getDeliveryProgress(getConnectUser(), job) - // Java interop — use the Flow-returning equivalents from Kotlin. + // Java interop — use the Flow-returning equivalents from Kotlin. - fun retrieveOpportunitiesForJava(listener: ConnectActivityCompleteListener) { - CoroutineScope(DispatcherProvider.io()).launch { - getOpportunities(forceRefresh = true).collect { state -> - when (state) { - is DataState.Success -> { - withContext(DispatcherProvider.main()) { - listener.connectActivityComplete( - true - ) - } - } + fun retrieveOpportunitiesForJava(listener: ConnectActivityCompleteListener) = + getOpportunities(forceRefresh = true).launchForJava(listener) - is DataState.Error -> { - withContext(DispatcherProvider.main()) { - listener.connectActivityComplete( - false - ) - } - } - - else -> {} - } - } - } - } + fun updateDeliveryProgressForJava( + job: ConnectJobRecord, + listener: ConnectActivityCompleteListener, + ) = getDeliveryProgress(job).launchForJava(listener) - fun updateDeliveryProgressForJava( - job: ConnectJobRecord, - listener: ConnectActivityCompleteListener, - ) { - CoroutineScope(DispatcherProvider.io()).launch { - getDeliveryProgress(job).collect { state -> - when (state) { - is DataState.Success -> { - withContext(DispatcherProvider.main()) { - listener.connectActivityComplete( - true - ) - } - } + fun updatePaymentsConfirmedForJava( + paymentConfirmations: List, + listener: ConnectActivityCompleteListener, + ) = confirmPayments(paymentConfirmations).launchForJava(listener) - is DataState.Error -> { - withContext(DispatcherProvider.main()) { - listener.connectActivityComplete( - false - ) + private fun Flow>.launchForJava(listener: ConnectActivityCompleteListener) { + CoroutineScope(DispatcherProvider.io()).launch { + collect { state -> + when (state) { + is DataState.Success -> { + withContext(DispatcherProvider.main()) { + listener.connectActivityComplete(true) + } } - } - else -> {} - } - } - } - } - - fun updatePaymentsConfirmedForJava( - paymentConfirmations: List, - listener: ConnectActivityCompleteListener, - ) { - CoroutineScope(DispatcherProvider.io()).launch { - confirmPayments(paymentConfirmations).collect { state -> - when (state) { - is DataState.Success -> { - withContext(DispatcherProvider.main()) { - listener.connectActivityComplete( - true - ) + is DataState.Error -> { + withContext(DispatcherProvider.main()) { + listener.connectActivityComplete( + false, + PersonalIdOrConnectApiErrorHandler.handle( + CommCareApplication.instance(), + state.errorCode, + state.throwable, + ), + ) + } } - } - is DataState.Error -> { - withContext(DispatcherProvider.main()) { - listener.connectActivityComplete( - false - ) - } + else -> {} } - - else -> {} } } } } - } diff --git a/app/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModel.kt b/app/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModel.kt index e399825f4b..3c7781ca26 100644 --- a/app/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModel.kt +++ b/app/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.Job -import org.commcare.android.database.connect.models.ConnectUserRecord import org.commcare.connect.repository.ConnectRepository import org.commcare.connect.repository.DataState @@ -20,14 +19,11 @@ class ConnectJobIntroViewModel( private var startLearningJob: Job? = null - fun startLearning( - user: ConnectUserRecord, - jobUUID: String, - ) { + fun startLearning(jobUUID: String) { startLearningJob?.cancel() startLearningJob = collectInto( - flow = repository.startLearning(user, jobUUID), + flow = repository.startLearning(jobUUID), liveData = _startLearning, ) } diff --git a/app/src/org/commcare/fragments/connect/ConnectDeliveryPaymentFragment.java b/app/src/org/commcare/fragments/connect/ConnectDeliveryPaymentFragment.java index b53a95b963..550d2fb25e 100644 --- a/app/src/org/commcare/fragments/connect/ConnectDeliveryPaymentFragment.java +++ b/app/src/org/commcare/fragments/connect/ConnectDeliveryPaymentFragment.java @@ -25,7 +25,7 @@ import org.commcare.android.database.connect.models.ConnectJobPaymentRecord; import org.commcare.android.database.connect.models.ConnectJobRecord; import org.commcare.connect.ConnectDateUtils; -import org.commcare.connect.ConnectJobHelper; +import org.commcare.connect.repository.ConnectRepository; import org.commcare.connect.database.ConnectJobUtils; import org.commcare.connect.network.connect.models.ConnectPaymentConfirmationModel; import org.commcare.dalvik.R; @@ -163,12 +163,11 @@ private void handleConfirmationDialogResult( ConnectJobPaymentRecord payment, boolean result ) { - ConnectJobHelper.INSTANCE.updatePaymentsConfirmed( - context, + ConnectRepository.getInstance(context).updatePaymentsConfirmedForJava( Collections.singletonList( new ConnectPaymentConfirmationModel(payment, result) ), - (success,error) -> { + (success, error) -> { holder.updateConfirmedText(context, payment); rebuildPaymentsDisplayList(); notifyDataSetChanged(); diff --git a/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java b/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java index ba5ec28af7..ff62039458 100644 --- a/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java +++ b/app/src/org/commcare/fragments/connect/ConnectDeliveryProgressFragment.java @@ -23,7 +23,6 @@ import org.commcare.android.database.connect.models.ConnectJobPaymentRecord; import org.commcare.connect.ConnectAppLaunchController; import org.commcare.connect.ConnectDateUtils; -import org.commcare.connect.ConnectJobHelper; import org.commcare.connect.PersonalIdManager; import org.commcare.connect.database.ConnectJobUtils; import org.commcare.connect.network.connect.models.ConnectPaymentConfirmationModel; @@ -196,8 +195,7 @@ private void handlePaymentConfirmYesButtonClick() { } FirebaseAnalyticsUtil.reportCccPaymentConfirmationInteraction(true); - ConnectJobHelper.INSTANCE.updatePaymentsConfirmed( - requireContext(), + ConnectRepository.getInstance(requireContext()).updatePaymentsConfirmedForJava( paymentsToConfirm, (success, error) -> { if (isAdded()) { diff --git a/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt b/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt index f25106b10b..3ed8e53b50 100644 --- a/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt +++ b/app/src/org/commcare/fragments/connect/ConnectJobIntroFragment.kt @@ -13,7 +13,6 @@ import org.commcare.connect.ConnectAppLaunchController import org.commcare.connect.ConnectDateUtils import org.commcare.connect.ConnectMoneyUtils import org.commcare.connect.database.ConnectJobUtils -import org.commcare.connect.database.ConnectUserDatabaseUtil import org.commcare.connect.network.PersonalIdOrConnectApiErrorHandler import org.commcare.connect.repository.DataState import org.commcare.connect.viewmodel.ConnectJobIntroViewModel @@ -169,8 +168,7 @@ class ConnectJobIntroFragment : ConnectJobFragment>() { - - @Override - public void onFailure(@NonNull PersonalIdOrConnectApiErrorCodes errorCode, - @androidx.annotation.Nullable Throwable t) { - if (!isAdded()) { return; } - - tryToLoadInvitedOpp(false); - setFragmentRedirection(); - } - - @Override - public void onSuccess(List jobs) { - if (!isAdded()) { return; } - if (!jobs.isEmpty()) { - ConnectUserDatabaseUtil.turnOnConnectAccess(requireContext()); + ConnectRepository.getInstance(requireContext()).retrieveOpportunitiesForJava( + (success, error) -> { + if (!isAdded()) return; + if (success && !ConnectJobUtils.getCompositeJobs( + requireContext(), ConnectJobRecord.STATUS_ALL_JOBS, null).isEmpty()) { + ConnectUserDatabaseUtil.turnOnConnectAccess(requireContext()); + } + tryToLoadInvitedOpp(success); + setFragmentRedirection(); } - - tryToLoadInvitedOpp(true); - setFragmentRedirection(); - } - }.getConnectOpportunities(requireContext(), user); + ); } private void tryToLoadInvitedOpp(boolean refreshSucceeded) { diff --git a/app/src/org/commcare/login/PostLoginSideEffects.kt b/app/src/org/commcare/login/PostLoginSideEffects.kt index bca7fa53d0..8241e76a97 100644 --- a/app/src/org/commcare/login/PostLoginSideEffects.kt +++ b/app/src/org/commcare/login/PostLoginSideEffects.kt @@ -1,14 +1,12 @@ package org.commcare.login import android.content.Context -import kotlinx.coroutines.suspendCancellableCoroutine import org.commcare.CommCareApplication import org.commcare.activities.LoginActivity -import org.commcare.connect.ConnectActivityCompleteListener import org.commcare.connect.ConnectAppUtils -import org.commcare.connect.ConnectJobHelper import org.commcare.connect.PersonalIdManager import org.commcare.connect.database.ConnectJobUtils +import org.commcare.connect.repository.ConnectRepository import org.commcare.utils.CrashUtil /** @@ -42,18 +40,7 @@ internal class PostLoginSideEffects( ConnectAppUtils.updateLastAccessed(context, appId, username) - suspendCancellableCoroutine { continuation -> - val listener = - object : ConnectActivityCompleteListener { - override fun connectActivityComplete( - success: Boolean, - error: String?, - ) { - continuation.resumeOnce(success) - } - } - ConnectJobHelper.updateJobProgress(context, job, listener) - } + ConnectRepository.getInstance(context).syncJobProgress(job).collect {} return PostLoginOutcome( redirectToConnectOpportunityInfo = job.isUserSuspended, diff --git a/app/src/org/commcare/pn/workers/NotificationsSyncWorker.kt b/app/src/org/commcare/pn/workers/NotificationsSyncWorker.kt index 75ef52a430..d01c3fff22 100644 --- a/app/src/org/commcare/pn/workers/NotificationsSyncWorker.kt +++ b/app/src/org/commcare/pn/workers/NotificationsSyncWorker.kt @@ -14,24 +14,22 @@ import org.commcare.android.database.connect.models.ConnectJobRecord.STATUS_AVAI import org.commcare.android.database.connect.models.ConnectJobRecord.STATUS_AVAILABLE_NEW import org.commcare.android.database.connect.models.ConnectJobRecord.STATUS_DELIVERING import org.commcare.android.database.connect.models.ConnectJobRecord.STATUS_LEARNING -import org.commcare.connect.ConnectActivityCompleteListener import org.commcare.connect.ConnectConstants.NOTIFICATION_BODY import org.commcare.connect.ConnectConstants.NOTIFICATION_ID import org.commcare.connect.ConnectConstants.OPPORTUNITY_STATUS import org.commcare.connect.ConnectConstants.OPPORTUNITY_STATUS_DELIVERY import org.commcare.connect.ConnectConstants.OPPORTUNITY_STATUS_LEARN import org.commcare.connect.ConnectConstants.OPPORTUNITY_UUID -import org.commcare.connect.ConnectJobHelper import org.commcare.connect.database.ConnectJobUtils import org.commcare.connect.database.NotificationRecordDatabaseHelper.getNotificationById +import org.commcare.connect.repository.ConnectRepository +import org.commcare.connect.repository.DataState import org.commcare.dalvik.R import org.commcare.util.LogTypes import org.commcare.utils.FirebaseMessagingUtil import org.commcare.utils.FirebaseMessagingUtil.cccCheckPassed import org.commcare.utils.PushNotificationApiHelper import org.javarosa.core.services.Logger -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine /** * This worker is responsible to sync different API endpoints from Connect and PersonalID server based on the action @@ -124,20 +122,26 @@ class NotificationsSyncWorker( } } - private suspend fun syncOpportunities(): PNApiResponseStatus = - suspendCoroutine { continuation -> - ConnectJobHelper.retrieveOpportunities( - appContext, - object : ConnectActivityCompleteListener { - override fun connectActivityComplete( - success: Boolean, - error: String?, - ) { - continuation.resume(PNApiResponseStatus(success, !success)) + private suspend fun syncOpportunities(): PNApiResponseStatus { + var success = false + ConnectRepository + .getInstance(appContext) + .getOpportunities(forceRefresh = true) + .collect { state -> + when (state) { + is DataState.Success -> { + success = true } - }, - ) - } + + is DataState.Error -> { + success = false + } + + else -> {} + } + } + return PNApiResponseStatus(success, !success) + } private suspend fun syncPersonalIdNotifications(): PNApiResponseStatus { val result = PushNotificationApiHelper.retrieveLatestPushNotifications(appContext) @@ -146,24 +150,25 @@ class NotificationsSyncWorker( private suspend fun syncJobProgress(): PNApiResponseStatus { job = getConnectJob() - return if (job == null) { - handleNoConnectJob() - } else { - suspendCoroutine { continuation -> - ConnectJobHelper.updateJobProgress( - appContext, - job!!, - object : ConnectActivityCompleteListener { - override fun connectActivityComplete( - success: Boolean, - error: String?, - ) { - continuation.resume(PNApiResponseStatus(success, !success)) - } - }, - ) + if (job == null) return handleNoConnectJob() + var success = false + ConnectRepository + .getInstance(appContext) + .syncJobProgress(job!!) + .collect { state -> + when (state) { + is DataState.Success -> { + success = true + } + + is DataState.Error -> { + success = false + } + + else -> {} + } } - } + return PNApiResponseStatus(success, !success) } private fun checkForOpportunityStatus(): Boolean { diff --git a/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt b/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt index 5929713538..7170e30033 100644 --- a/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt +++ b/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt @@ -44,14 +44,14 @@ class ConnectJobIntroViewModelTest { @Test fun testStartLearning_postsLoadingThenSuccess() { - every { mockRepository.startLearning(any(), any()) } returns + every { mockRepository.startLearning(any()) } returns flowOf(DataState.Loading, DataState.Success(Unit)) val results = mutableListOf>() viewModel.startLearning.observeForever { results.add(it) } mainCoroutineRule.runBlockingTest { - viewModel.startLearning(mockk(), "test-uuid") + viewModel.startLearning("test-uuid") } assertEquals(2, results.size) @@ -61,14 +61,14 @@ class ConnectJobIntroViewModelTest { @Test fun testStartLearning_postsError_onFailure() { - every { mockRepository.startLearning(any(), any()) } returns + every { mockRepository.startLearning(any()) } returns flowOf(DataState.Loading, DataState.Error()) val results = mutableListOf>() viewModel.startLearning.observeForever { results.add(it) } mainCoroutineRule.runBlockingTest { - viewModel.startLearning(mockk(), "test-uuid") + viewModel.startLearning("test-uuid") } assertEquals(2, results.size) diff --git a/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt b/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt index 6b41be0ad8..38002cc684 100644 --- a/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt +++ b/app/unit-tests/src/org/commcare/fragments/connect/ConnectJobIntroFragmentTest.kt @@ -189,7 +189,7 @@ class ConnectJobIntroFragmentTest : BaseConnectJobIntroTest() { val jobUuidSlot = slot() val repo = ConnectRepository.getInstance(activity) - every { repo.startLearning(any(), capture(jobUuidSlot)) } returns + every { repo.startLearning(capture(jobUuidSlot)) } returns flow { emit(DataState.Success(Unit)) } From 16f66d66c7a654f22c6ecaf5b0bf4470e84b0cc2 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 7 Aug 2026 15:34:35 +0530 Subject: [PATCH 5/9] Delete ConnectApiHandler, ApiConnect, ConnectApiClient; clean up ApiService --- .../commcare/connect/network/ApiConnect.java | 151 ------------------ .../commcare/connect/network/ApiService.java | 22 --- .../network/connect/ConnectApiClient.kt | 30 ---- .../network/connect/ConnectApiHandler.kt | 77 --------- .../network/connect/ConnectNetworkClient.kt | 7 +- .../connect/network/ConnectMockApiServer.kt | 18 +-- 6 files changed, 9 insertions(+), 296 deletions(-) delete mode 100644 app/src/org/commcare/connect/network/ApiConnect.java delete mode 100644 app/src/org/commcare/connect/network/connect/ConnectApiClient.kt delete mode 100644 app/src/org/commcare/connect/network/connect/ConnectApiHandler.kt diff --git a/app/src/org/commcare/connect/network/ApiConnect.java b/app/src/org/commcare/connect/network/ApiConnect.java deleted file mode 100644 index b3c4b743f6..0000000000 --- a/app/src/org/commcare/connect/network/ApiConnect.java +++ /dev/null @@ -1,151 +0,0 @@ -package org.commcare.connect.network; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import org.commcare.android.database.connect.models.ConnectUserRecord; -import org.commcare.connect.network.base.BaseApi; -import org.commcare.connect.network.connect.ConnectApiClient; -import org.commcare.connect.network.connect.models.ConnectPaymentConfirmationModel; -import org.commcare.core.network.AuthInfo; -import org.commcare.network.HttpUtils; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import okhttp3.RequestBody; -import okhttp3.ResponseBody; -import retrofit2.Call; - -public class ApiConnect { - public static final String API_VERSION_CONNECT = "1.0"; - - public static void getConnectOpportunities(Context context, @NonNull ConnectUserRecord user, IApiCallback callback) { - - ConnectSsoHelper.retrievePersonalIdToken(context, user, new ConnectSsoHelper.TokenCallback() { - @Override - public void tokenRetrieved(AuthInfo.TokenAuth token) { - - String tokenAuth = HttpUtils.getCredential(token); - ApiService apiService = ConnectApiClient.Companion.getClientApi(); - HashMap headers = new HashMap<>(); - ConnectNetworkHelper.addVersionHeader(headers, API_VERSION_CONNECT); - Call call = apiService.getConnectOpportunities(tokenAuth,headers); - BaseApi.Companion.callApi(context, call, callback,ApiEndPoints.connectOpportunitiesURL); - } - - @Override - public void tokenUnavailable() { - callback.processTokenUnavailableError(); - } - - @Override - public void tokenRequestDenied() { - callback.processTokenRequestDeniedError(); - } - }); - - } - - public static void startLearnApp(Context context, @NonNull ConnectUserRecord user, String jobUUID, IApiCallback callback) { - - ConnectSsoHelper.retrievePersonalIdToken(context, user, new ConnectSsoHelper.TokenCallback() { - @Override - public void tokenRetrieved(AuthInfo.TokenAuth token) { - HashMap params = new HashMap<>(); - params.put("opportunity", jobUUID); - - HashMap headers = new HashMap<>(); - RequestBody requestBody = ConnectNetworkHelper.buildPostFormHeaders(params, true, API_VERSION_CONNECT, headers); - - String tokenAuth = HttpUtils.getCredential(token); - ApiService apiService = ConnectApiClient.Companion.getClientApi(); - Call call = apiService.connectStartLearningApp(tokenAuth,headers,requestBody); - BaseApi.Companion.callApi(context, call, callback,ApiEndPoints.connectStartLearningURL); - } - - @Override - public void tokenUnavailable() { - callback.processTokenUnavailableError(); - } - - @Override - public void tokenRequestDenied() { - callback.processTokenRequestDeniedError(); - } - }); - - } - - public static void claimJob(Context context, @NonNull ConnectUserRecord user, String jobUUID, IApiCallback callback) { - ConnectSsoHelper.retrievePersonalIdToken(context, user, new ConnectSsoHelper.TokenCallback() { - @Override - public void tokenRetrieved(AuthInfo.TokenAuth token) { - - HashMap params = new HashMap<>(); - HashMap headers = new HashMap<>(); - RequestBody requestBody = ConnectNetworkHelper.buildPostFormHeaders(params, false, API_VERSION_CONNECT, headers); - - String tokenAuth = HttpUtils.getCredential(token); - ApiService apiService = ConnectApiClient.Companion.getClientApi(); - Call call = apiService.connectClaimJob(tokenAuth,jobUUID,headers,requestBody); - BaseApi.Companion.callApi(context, call, callback,ApiEndPoints.connectClaimJobURL); - } - - @Override - public void tokenUnavailable() { - callback.processTokenUnavailableError(); - } - - @Override - public void tokenRequestDenied() { - callback.processTokenRequestDeniedError(); - } - }); - - } - - public static void setPaymentsConfirmed( - Context context, - @NonNull ConnectUserRecord user, - List paymentConfirmations, - IApiCallback callback - ) { - ConnectSsoHelper.retrievePersonalIdToken(context, user, new ConnectSsoHelper.TokenCallback() { - @Override - public void tokenRetrieved(AuthInfo.TokenAuth token) { - List> paymentConfirmationsMap = new ArrayList<>(); - for (ConnectPaymentConfirmationModel paymentConfirmation : paymentConfirmations) { - HashMap paymentMap = new HashMap<>(); - paymentMap.put("id", paymentConfirmation.getPayment().getPaymentUUID()); - paymentMap.put("confirmed", paymentConfirmation.getToConfirm() ? "true" : "false"); - paymentConfirmationsMap.add(paymentMap); - } - - HashMap params = new HashMap<>(); - params.put("payments", paymentConfirmationsMap); - - HashMap headers = new HashMap<>(); - RequestBody requestBody = ConnectNetworkHelper.buildPostFormHeaders(params, false, API_VERSION_CONNECT, headers); - - String tokenAuth = HttpUtils.getCredential(token); - ApiService apiService = ConnectApiClient.Companion.getClientApi(); - Call call = apiService.connectPaymentConfirmations(tokenAuth, headers, requestBody); - BaseApi.Companion.callApi(context, call, callback, ApiEndPoints.PAYMENT_CONFIRMAITONS); - } - - @Override - public void tokenUnavailable() { - callback.processTokenUnavailableError(); - } - - @Override - public void tokenRequestDenied() { - callback.processTokenRequestDeniedError(); - } - }); - } -} diff --git a/app/src/org/commcare/connect/network/ApiService.java b/app/src/org/commcare/connect/network/ApiService.java index 824ddf1421..8d886f60b6 100644 --- a/app/src/org/commcare/connect/network/ApiService.java +++ b/app/src/org/commcare/connect/network/ApiService.java @@ -10,7 +10,6 @@ import retrofit2.http.Header; import retrofit2.http.HeaderMap; import retrofit2.http.POST; -import retrofit2.http.Path; import retrofit2.http.Url; public interface ApiService { @@ -48,27 +47,6 @@ Call confirmBackupCode(@Header("Authorization") String token, @GET(ApiEndPoints.CREDENTIALS) Call retrieveCredentials(@Header("Authorization") String token); - - @GET(ApiEndPoints.connectOpportunitiesURL) - Call getConnectOpportunities(@Header("Authorization") String token, @HeaderMap Map headers); - - - @POST(ApiEndPoints.connectStartLearningURL) - Call connectStartLearningApp(@Header("Authorization") String token, - @HeaderMap Map headers, - @Body RequestBody connectStartLearningRequest); - - @POST(ApiEndPoints.connectClaimJobURL) - Call connectClaimJob(@Header("Authorization") String token, - @Path("id") String uuid, - @HeaderMap Map headers, - @Body RequestBody connectClaimJobRequest); - - @POST(ApiEndPoints.PAYMENT_CONFIRMAITONS) - Call connectPaymentConfirmations(@Header("Authorization") String token, - @HeaderMap Map headers, - @Body RequestBody connectPaymentConfirmationsRequest); - @POST(ApiEndPoints.sendSessionOtp) Call sendSessionOtp(@Header("Authorization") String token); diff --git a/app/src/org/commcare/connect/network/connect/ConnectApiClient.kt b/app/src/org/commcare/connect/network/connect/ConnectApiClient.kt deleted file mode 100644 index a5d187b575..0000000000 --- a/app/src/org/commcare/connect/network/connect/ConnectApiClient.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.commcare.connect.network.connect - -import org.commcare.connect.network.base.BaseApiClient.buildRetrofitClient -import org.commcare.connect.network.ApiService -import org.commcare.dalvik.BuildConfig - -/** - * Retrofit client for Connect API - */ -class ConnectApiClient { - - companion object { - - val BASE_URL: String = "https://${BuildConfig.CCC_HOST}" - private var apiService: ApiService? = null - - fun getClientApi(): ApiService { - if (apiService == null) { - synchronized(ConnectApiClient::class.java) { // Double-checked locking - if (apiService == null) { - apiService = buildRetrofitClient(BASE_URL).create(ApiService::class.java) - } - } - } - return apiService!! - } - } - - -} \ No newline at end of file diff --git a/app/src/org/commcare/connect/network/connect/ConnectApiHandler.kt b/app/src/org/commcare/connect/network/connect/ConnectApiHandler.kt deleted file mode 100644 index 75bf1d88d1..0000000000 --- a/app/src/org/commcare/connect/network/connect/ConnectApiHandler.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.commcare.connect.network.connect - -import android.content.Context -import org.commcare.android.database.connect.models.ConnectUserRecord -import org.commcare.connect.network.ApiConnect -import org.commcare.connect.network.NoParsingResponseParser -import org.commcare.connect.network.base.BaseApiHandler -import org.commcare.connect.network.connect.models.ConnectPaymentConfirmationModel -import org.commcare.connect.network.connect.parser.ConnectOpportunitiesParser -import org.commcare.interfaces.base.BaseConnectView - -/** - * Class for all connect api handlers - */ -abstract class ConnectApiHandler( - loading: Boolean? = false, - view: BaseConnectView? = null, -) : BaseApiHandler(loading, view) { - fun getConnectOpportunities( - context: Context, - user: ConnectUserRecord, - ) { - ApiConnect.getConnectOpportunities( - context, - user, - createCallback( - ConnectOpportunitiesParser(), - context, - ), - ) - } - - fun connectStartLearning( - context: Context, - user: ConnectUserRecord, - jobUUID: String, - ) { - ApiConnect.startLearnApp( - context, - user, - jobUUID, - createCallback( - NoParsingResponseParser(), - ), - ) - } - - fun claimJob( - context: Context, - user: ConnectUserRecord, - jobUUID: String, - ) { - ApiConnect.claimJob( - context, - user, - jobUUID, - createCallback( - NoParsingResponseParser(), - ), - ) - } - - fun setPaymentConfirmations( - context: Context, - user: ConnectUserRecord, - paymentConfirmations: List, - ) { - ApiConnect.setPaymentsConfirmed( - context, - user, - paymentConfirmations, - createCallback( - NoParsingResponseParser(), - ), - ) - } -} diff --git a/app/src/org/commcare/connect/network/connect/ConnectNetworkClient.kt b/app/src/org/commcare/connect/network/connect/ConnectNetworkClient.kt index 6ca47ae357..18bf426d4c 100644 --- a/app/src/org/commcare/connect/network/connect/ConnectNetworkClient.kt +++ b/app/src/org/commcare/connect/network/connect/ConnectNetworkClient.kt @@ -4,7 +4,6 @@ import androidx.annotation.VisibleForTesting import okhttp3.ResponseBody import org.commcare.android.database.connect.models.ConnectJobRecord import org.commcare.android.database.connect.models.ConnectUserRecord -import org.commcare.connect.network.ApiConnect.API_VERSION_CONNECT import org.commcare.connect.network.ConnectApiService import org.commcare.connect.network.ConnectNetworkHelper import org.commcare.connect.network.base.BaseApiClient @@ -20,6 +19,7 @@ import org.commcare.connect.network.connect.parser.DeliveryAppProgressResponsePa import org.commcare.connect.network.connect.parser.LearningAppProgressResponseParser import org.commcare.connect.network.getAuthorizationHeader import org.commcare.connect.network.mapHttpErrorCode +import org.commcare.dalvik.BuildConfig import retrofit2.Response import java.io.IOException import java.io.InputStream @@ -30,6 +30,9 @@ class ConnectNetworkClient private val apiService: ConnectApiService, ) { companion object { + private const val BASE_URL = "https://${BuildConfig.CCC_HOST}" + private const val API_VERSION_CONNECT = "1.0" + @Volatile private var instance: ConnectNetworkClient? = null @@ -37,7 +40,7 @@ class ConnectNetworkClient instance ?: synchronized(this) { instance ?: ConnectNetworkClient( BaseApiClient - .buildRetrofitClient(ConnectApiClient.BASE_URL) + .buildRetrofitClient(BASE_URL) .create(ConnectApiService::class.java), ).also { instance = it } } diff --git a/app/unit-tests/src/org/commcare/connect/network/ConnectMockApiServer.kt b/app/unit-tests/src/org/commcare/connect/network/ConnectMockApiServer.kt index a0e5489234..ef2fa34868 100644 --- a/app/unit-tests/src/org/commcare/connect/network/ConnectMockApiServer.kt +++ b/app/unit-tests/src/org/commcare/connect/network/ConnectMockApiServer.kt @@ -8,20 +8,18 @@ import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.RecordedRequest import org.commcare.connect.network.base.BaseApiClient -import org.commcare.connect.network.connect.ConnectApiClient import org.commcare.connect.network.connect.ConnectNetworkClient import org.robolectric.shadows.ShadowLooper import java.util.concurrent.TimeUnit /** - * [MockWebServer] harness that points the Connect API clients at a local mock server so Connect API + * [MockWebServer] harness that points [ConnectNetworkClient] at a local mock server so Connect API * calls hit it. The PersonalID equivalent lives in `PersonalIdMockApiServer`; the two target * different Retrofit clients. * - * Both [ConnectApiClient] and [ConnectNetworkClient] are process-wide singletons, so [start] swaps - * their backing instances and [shutdown] must restore them. Callers seat a Connect user with a valid - * token (see `ConnectTestUtils`) so the calls don't detour to the PersonalId token endpoint, which - * this server doesn't serve. + * [ConnectNetworkClient] is a process-wide singleton, so [start] swaps its backing instance and + * [shutdown] must restore it. Callers seat a Connect user with a valid token (see `ConnectTestUtils`) + * so the calls don't detour to the PersonalId token endpoint, which this server doesn't serve. * * Retrofit posts callbacks to the main looper as it does in production, and [drainHttp] runs them * deterministically. Calls that resume on a background dispatcher and only then post their result to @@ -51,13 +49,11 @@ class ConnectMockApiServer { Handler(Looper.getMainLooper()).post(runnable) }.build() httpDispatcher = (retrofit.callFactory() as OkHttpClient).dispatcher - setConnectApiService(retrofit.create(ApiService::class.java)) setNetworkClient(ConnectNetworkClient(retrofit.create(ConnectApiService::class.java))) } fun shutdown() { dispatchCallbacks = false - setConnectApiService(null) setNetworkClient(null) server.shutdown() } @@ -130,12 +126,6 @@ class ConnectMockApiServer { ShadowLooper.idleMainLooper() } - private fun setConnectApiService(apiService: ApiService?) { - val apiServiceField = ConnectApiClient::class.java.getDeclaredField("apiService") - apiServiceField.isAccessible = true - apiServiceField.set(null, apiService) - } - /** The companion's backing field is a static on [ConnectNetworkClient] itself, and is private. */ private fun setNetworkClient(client: ConnectNetworkClient?) { ConnectNetworkClient::class.java From f4a86ef047bf5d985b334258eaef60ce523117ed Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Fri, 14 Aug 2026 18:40:16 +0530 Subject: [PATCH 6/9] Replace ConnectRepository mocks with real HTTP mocks in fragment and login tests - ConnectLearningProgressFragmentTest: remove all spyk/mockk of ConnectRepository; seed user before activity creation so background getOpportunities call finds a user; pre-enqueue and drain getOpportunities and getLearningProgress responses so they don't pollute the claim request queue; all 7 tests pass against the real repository and ConnectMockApiServer - ConnectRepository: add @VisibleForTesting resetInstance() so tests can force a fresh singleton backed by the mock HTTP client - PostLoginSideEffects: make ConnectRepository injectable for unit testing - PostLoginSideEffectsTest: rewrite to use injected mock repository instead of mocking the singleton Co-Authored-By: Claude Sonnet 4.6 --- .../connect/repository/ConnectRepository.kt | 5 ++ .../commcare/login/PostLoginSideEffects.kt | 3 +- .../repository/ConnectRepositoryTest.kt | 10 +++- .../ConnectLearningProgressFragmentTest.kt | 56 +++++++++++-------- .../login/PostLoginSideEffectsTest.kt | 27 ++++----- 5 files changed, 59 insertions(+), 42 deletions(-) diff --git a/app/src/org/commcare/connect/repository/ConnectRepository.kt b/app/src/org/commcare/connect/repository/ConnectRepository.kt index 8d2f9f2429..61b5680808 100644 --- a/app/src/org/commcare/connect/repository/ConnectRepository.kt +++ b/app/src/org/commcare/connect/repository/ConnectRepository.kt @@ -51,6 +51,11 @@ class ConnectRepository ConnectNetworkClient.getInstance(), ).also { instance = it } } + + @VisibleForTesting + internal fun resetInstance() { + instance = null + } } fun getOpportunities( diff --git a/app/src/org/commcare/login/PostLoginSideEffects.kt b/app/src/org/commcare/login/PostLoginSideEffects.kt index 8241e76a97..569baa910a 100644 --- a/app/src/org/commcare/login/PostLoginSideEffects.kt +++ b/app/src/org/commcare/login/PostLoginSideEffects.kt @@ -16,6 +16,7 @@ import org.commcare.utils.CrashUtil internal class PostLoginSideEffects( private val context: Context, private val personalIdManager: PersonalIdManager = PersonalIdManager.getInstance(), + private val repository: ConnectRepository = ConnectRepository.getInstance(context), ) { suspend fun runOnSuccess(username: String): PostLoginOutcome { CrashUtil.registerUserData() @@ -40,7 +41,7 @@ internal class PostLoginSideEffects( ConnectAppUtils.updateLastAccessed(context, appId, username) - ConnectRepository.getInstance(context).syncJobProgress(job).collect {} + repository.syncJobProgress(job).collect {} return PostLoginOutcome( redirectToConnectOpportunityInfo = job.isUserSuspended, diff --git a/app/unit-tests/src/org/commcare/connect/repository/ConnectRepositoryTest.kt b/app/unit-tests/src/org/commcare/connect/repository/ConnectRepositoryTest.kt index c5ab1fffe4..be23fe44c6 100644 --- a/app/unit-tests/src/org/commcare/connect/repository/ConnectRepositoryTest.kt +++ b/app/unit-tests/src/org/commcare/connect/repository/ConnectRepositoryTest.kt @@ -1,9 +1,11 @@ package org.commcare.connect.repository import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.mockk.Runs import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll @@ -33,6 +35,7 @@ class ConnectRepositoryTest { private lateinit var mockSyncPrefs: ConnectSyncPreferences private lateinit var mockNetworkClient: ConnectNetworkClient private lateinit var mockUser: ConnectUserRecord + private lateinit var mockJob: ConnectJobRecord private lateinit var repository: ConnectRepository @Before @@ -40,11 +43,14 @@ class ConnectRepositoryTest { mockSyncPrefs = mockk(relaxed = true) mockNetworkClient = mockk() mockUser = mockk() + mockJob = mockk(relaxed = true) // Static mocks for database utilities mockkStatic(ConnectJobUtils::class) mockkStatic(ConnectUserDatabaseUtil::class) every { ConnectUserDatabaseUtil.getUser(any()) } returns mockUser + every { ConnectJobUtils.upsertJob(any()) } just Runs + every { ConnectJobUtils.storePayment(any(), any()) } just Runs repository = ConnectRepository(mockSyncPrefs, mockNetworkClient) } @@ -294,7 +300,7 @@ class ConnectRepositoryTest { runBlocking { coEvery { mockNetworkClient.claimJob(any(), any()) } returns Result.success(Unit) - val emissions = repository.claimJob("test-uuid").toList() + val emissions = repository.claimJob(mockJob).toList() assertEquals(2, emissions.size) assertTrue(emissions[0] is DataState.Loading) @@ -307,7 +313,7 @@ class ConnectRepositoryTest { coEvery { mockNetworkClient.claimJob(any(), any()) } returns Result.failure(Exception("Network error")) - val emissions = repository.claimJob("test-uuid").toList() + val emissions = repository.claimJob(mockJob).toList() assertEquals(2, emissions.size) assertTrue(emissions[0] is DataState.Loading) diff --git a/app/unit-tests/src/org/commcare/fragments/connect/ConnectLearningProgressFragmentTest.kt b/app/unit-tests/src/org/commcare/fragments/connect/ConnectLearningProgressFragmentTest.kt index 256c87d0d4..b45e321905 100644 --- a/app/unit-tests/src/org/commcare/fragments/connect/ConnectLearningProgressFragmentTest.kt +++ b/app/unit-tests/src/org/commcare/fragments/connect/ConnectLearningProgressFragmentTest.kt @@ -1,19 +1,21 @@ package org.commcare.fragments.connect +import android.content.Context import android.os.Build -import android.os.Bundle import android.view.View import androidx.navigation.NavController -import androidx.navigation.NavDestination import androidx.navigation.fragment.NavHostFragment +import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.android.material.button.MaterialButton +import io.mockk.Runs import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject import io.mockk.mockkStatic import io.mockk.unmockkAll -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.commcare.AppUtils @@ -30,8 +32,10 @@ import org.commcare.connect.database.ConnectJobUtils import org.commcare.connect.database.ConnectUserDatabaseUtil import org.commcare.connect.network.ConnectMockApiServer import org.commcare.connect.repository.ConnectRepository +import org.commcare.connect.repository.ConnectRequestManager import org.commcare.dalvik.R import org.commcare.google.services.analytics.FirebaseAnalyticsUtil +import org.commcare.utils.coroutines.DispatcherProvider import org.commcare.views.connect.ConnectSuccessFailureCard import org.junit.After import org.junit.Assert.assertEquals @@ -67,32 +71,30 @@ class ConnectLearningProgressFragmentTest { fun setUp() { savedStatus = PersonalIdManager.getInstance().status PersonalIdManager.getInstance().status = PersonalIdManager.PersonalIdStatus.LoggedIn + seedConnectUser(ApplicationProvider.getApplicationContext()) + mockApi.start() + ConnectRepository.resetInstance() + ConnectRequestManager.cancelAll() + + mockkObject(DispatcherProvider) + every { DispatcherProvider.io() } returns UnconfinedTestDispatcher() mockkStatic(MessageManager::class) every { MessageManager.retrieveMessages(any(), any()) } returns Unit mockkStatic(FirebaseAnalyticsUtil::class) - every { FirebaseAnalyticsUtil.getNavControllerPageChangeLoggingListener() } returns - object : NavController.OnDestinationChangedListener { - override fun onDestinationChanged( - controller: NavController, - destination: NavDestination, - arguments: Bundle?, - ) = Unit - } - every { FirebaseAnalyticsUtil.reportCccApiClaimJob(any()) } returns Unit - - // No emission: the screen must decide its state from the already-loaded active job. - mockkObject(ConnectRepository.Companion) - val repository = mockk(relaxed = true) - every { ConnectRepository.getInstance(any()) } returns repository - every { repository.getOpportunities(any(), any()) } returns emptyFlow() - every { repository.getLearningProgress(any(), any(), any()) } returns emptyFlow() + every { FirebaseAnalyticsUtil.getNavControllerPageChangeLoggingListener() } returns mockk(relaxed = true) + every { FirebaseAnalyticsUtil.reportCccApiClaimJob(any()) } just Runs + every { FirebaseAnalyticsUtil.reportCccApiLearnProgress(any()) } just Runs mockkStatic(AppUtils::class) every { AppUtils.isAppInstalled(any()) } returns false + // Pre-enqueue a response so the getOpportunities request made by the start destination + // does not hang, then drain it so it doesn't sit ahead of later requests in the queue. + mockApi.server.enqueue(MockResponse().setResponseCode(200).setBody("[]")) + activity = Robolectric .buildActivity(ConnectActivity::class.java) @@ -105,7 +107,8 @@ class ConnectLearningProgressFragmentTest { activity.supportFragmentManager .findFragmentById(R.id.nav_host_fragment_connect) as NavHostFragment - seedConnectUser() + mockApi.drainHttp() + ShadowLooper.idleMainLooper() } @After @@ -178,10 +181,11 @@ class ConnectLearningProgressFragmentTest { val job = ConnectLearnJobTestData.job() job.status = ConnectJobRecord.STATUS_DELIVERING val fragment = launch(job) + val requestsBefore = mockApi.server.requestCount clickCta(fragment) - assertEquals("No claim request should be sent", 0, mockApi.requestCount) + assertEquals("No claim request should be sent", requestsBefore, mockApi.server.requestCount) assertEquals(R.id.connect_downloading_fragment, navController.currentDestination?.id) } @@ -215,12 +219,18 @@ class ConnectLearningProgressFragmentTest { private fun launch(job: ConnectJobRecord): ConnectLearningProgressFragment { activity.setActiveJob(job) + // Pre-enqueue a 400 for getLearningProgress so it doesn't hang; a 400 error leaves the + // seeded job state intact (the observer ignores DataState.Error updates to the job field). + mockApi.server.enqueue(MockResponse().setResponseCode(400).setBody("{}")) activity.runOnUiThread { navController.navigate( R.id.action_connect_jobs_list_fragment_to_connect_job_learning_progress_fragment, ) } ShadowLooper.idleMainLooper() + // Drain the getLearningProgress request so it doesn't sit ahead of the claim request in the queue. + mockApi.drainHttp() + ShadowLooper.idleMainLooper() return navHostFragment.childFragmentManager.primaryNavigationFragment as ConnectLearningProgressFragment } @@ -248,7 +258,7 @@ class ConnectLearningProgressFragmentTest { * [ConnectDatabaseHelper.dbExists] has to be stubbed because it probes for the on-disk connect * db, which never exists under the in-memory test open helper. */ - private fun seedConnectUser() { + private fun seedConnectUser(context: Context = activity) { mockkStatic(ConnectDatabaseHelper::class) every { ConnectDatabaseHelper.dbExists() } returns true @@ -266,7 +276,7 @@ class ConnectLearningProgressFragmentTest { true, ) user.updateConnectToken("test-token", tomorrow()) - ConnectUserDatabaseUtil.storeUser(activity, user) + ConnectUserDatabaseUtil.storeUser(context, user) } private fun tomorrow(): Date = Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, 1) }.time diff --git a/app/unit-tests/src/org/commcare/login/PostLoginSideEffectsTest.kt b/app/unit-tests/src/org/commcare/login/PostLoginSideEffectsTest.kt index 299e20f5ee..86c5d7d2ca 100644 --- a/app/unit-tests/src/org/commcare/login/PostLoginSideEffectsTest.kt +++ b/app/unit-tests/src/org/commcare/login/PostLoginSideEffectsTest.kt @@ -1,23 +1,24 @@ package org.commcare.login import android.content.Context +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject import io.mockk.mockkStatic -import io.mockk.slot import io.mockk.unmockkAll import io.mockk.verify +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.commcare.CommCareApp import org.commcare.CommCareApplication import org.commcare.CommCareNoficationManager import org.commcare.android.database.connect.models.ConnectJobRecord -import org.commcare.connect.ConnectActivityCompleteListener import org.commcare.connect.ConnectAppUtils -import org.commcare.connect.ConnectJobHelper import org.commcare.connect.PersonalIdManager import org.commcare.connect.database.ConnectJobUtils +import org.commcare.connect.repository.ConnectRepository +import org.commcare.connect.repository.DataState import org.commcare.utils.CrashUtil import org.junit.After import org.junit.Assert.assertEquals @@ -30,13 +31,13 @@ class PostLoginSideEffectsTest { private val notificationManager = mockk(relaxed = true) private val commCareApplication = mockk(relaxed = true) private val currentApp = mockk(relaxed = true) + private val mockRepository = mockk(relaxed = true) @Before fun setUp() { mockkStatic(CommCareApplication::class) mockkStatic(CrashUtil::class) mockkStatic(ConnectJobUtils::class) - mockkObject(ConnectJobHelper) mockkObject(ConnectAppUtils) every { ConnectAppUtils.updateLastAccessed(any(), any(), any()) } returns Unit every { CommCareApplication.notificationManager() } returns notificationManager @@ -56,7 +57,7 @@ class PostLoginSideEffectsTest { runTest { every { personalIdManager.isloggedIn() } returns false - val outcome = PostLoginSideEffects(context, personalIdManager).runOnSuccess("alice") + val outcome = PostLoginSideEffects(context, personalIdManager, mockRepository).runOnSuccess("alice") assertEquals(PostLoginOutcome(redirectToConnectOpportunityInfo = false), outcome) verify { CrashUtil.registerUserData() } @@ -70,7 +71,7 @@ class PostLoginSideEffectsTest { every { personalIdManager.isloggedIn() } returns true every { ConnectJobUtils.getJobForApp(context, "app-1") } returns null - val outcome = PostLoginSideEffects(context, personalIdManager).runOnSuccess("alice") + val outcome = PostLoginSideEffects(context, personalIdManager, mockRepository).runOnSuccess("alice") assertEquals( PostLoginOutcome(redirectToConnectOpportunityInfo = false, needsPersonalIdLinkCheck = true), @@ -78,7 +79,7 @@ class PostLoginSideEffectsTest { ) verify { commCareApplication.setConnectJobIdForAnalytics(null) } verify(exactly = 0) { ConnectAppUtils.updateLastAccessed(any(), any(), any()) } - verify(exactly = 0) { ConnectJobHelper.updateJobProgress(any(), any(), any()) } + coVerify(exactly = 0) { mockRepository.syncJobProgress(any()) } } @Test @@ -87,18 +88,12 @@ class PostLoginSideEffectsTest { every { personalIdManager.isloggedIn() } returns true val job = mockk(relaxed = true) every { ConnectJobUtils.getJobForApp(context, "app-1") } returns job + every { mockRepository.syncJobProgress(job) } returns flowOf(DataState.Success(job)) - val listenerSlot = slot() - every { - ConnectJobHelper.updateJobProgress(context, job, capture(listenerSlot)) - } answers { - listenerSlot.captured.connectActivityComplete(true, "") - } - - PostLoginSideEffects(context, personalIdManager).runOnSuccess("alice") + PostLoginSideEffects(context, personalIdManager, mockRepository).runOnSuccess("alice") verify { commCareApplication.setConnectJobIdForAnalytics(job) } verify { ConnectAppUtils.updateLastAccessed(context, "app-1", "alice") } - verify { ConnectJobHelper.updateJobProgress(context, job, any()) } + coVerify { mockRepository.syncJobProgress(job) } } } From b3dec0c7ba8426c7bf4a92a98a800f0e50e81597 Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 18 Aug 2026 14:13:47 +0530 Subject: [PATCH 7/9] minor fixes on tests --- .../connect/network/ConnectMockApiServer.kt | 3 +++ .../connect/repository/ConnectRepositoryTest.kt | 14 +++++++++++--- .../viewmodel/ConnectJobIntroViewModelTest.kt | 5 ++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/unit-tests/src/org/commcare/connect/network/ConnectMockApiServer.kt b/app/unit-tests/src/org/commcare/connect/network/ConnectMockApiServer.kt index ef2fa34868..f7abe7d1bf 100644 --- a/app/unit-tests/src/org/commcare/connect/network/ConnectMockApiServer.kt +++ b/app/unit-tests/src/org/commcare/connect/network/ConnectMockApiServer.kt @@ -9,6 +9,7 @@ import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.RecordedRequest import org.commcare.connect.network.base.BaseApiClient import org.commcare.connect.network.connect.ConnectNetworkClient +import org.commcare.connect.repository.ConnectRepository import org.robolectric.shadows.ShadowLooper import java.util.concurrent.TimeUnit @@ -50,11 +51,13 @@ class ConnectMockApiServer { }.build() httpDispatcher = (retrofit.callFactory() as OkHttpClient).dispatcher setNetworkClient(ConnectNetworkClient(retrofit.create(ConnectApiService::class.java))) + ConnectRepository.resetInstance() } fun shutdown() { dispatchCallbacks = false setNetworkClient(null) + ConnectRepository.resetInstance() server.shutdown() } diff --git a/app/unit-tests/src/org/commcare/connect/repository/ConnectRepositoryTest.kt b/app/unit-tests/src/org/commcare/connect/repository/ConnectRepositoryTest.kt index be23fe44c6..56046f1cbc 100644 --- a/app/unit-tests/src/org/commcare/connect/repository/ConnectRepositoryTest.kt +++ b/app/unit-tests/src/org/commcare/connect/repository/ConnectRepositoryTest.kt @@ -13,11 +13,13 @@ import io.mockk.verify import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.commcare.CommCareTestApplication +import org.commcare.android.database.connect.models.ConnectJobPaymentRecord import org.commcare.android.database.connect.models.ConnectJobRecord import org.commcare.android.database.connect.models.ConnectUserRecord import org.commcare.connect.database.ConnectJobUtils import org.commcare.connect.database.ConnectUserDatabaseUtil import org.commcare.connect.network.connect.ConnectNetworkClient +import org.commcare.connect.network.connect.models.ConnectPaymentConfirmationModel import org.commcare.connect.network.connect.models.DeliveryAppProgressResponseModel import org.commcare.connect.network.connect.models.LearningAppProgressResponseModel import org.junit.After @@ -49,8 +51,6 @@ class ConnectRepositoryTest { mockkStatic(ConnectJobUtils::class) mockkStatic(ConnectUserDatabaseUtil::class) every { ConnectUserDatabaseUtil.getUser(any()) } returns mockUser - every { ConnectJobUtils.upsertJob(any()) } just Runs - every { ConnectJobUtils.storePayment(any(), any()) } just Runs repository = ConnectRepository(mockSyncPrefs, mockNetworkClient) } @@ -299,12 +299,15 @@ class ConnectRepositoryTest { fun testClaimJob_success_emitsLoadingThenSuccess() = runBlocking { coEvery { mockNetworkClient.claimJob(any(), any()) } returns Result.success(Unit) + every { ConnectJobUtils.upsertJob(any()) } just Runs val emissions = repository.claimJob(mockJob).toList() assertEquals(2, emissions.size) assertTrue(emissions[0] is DataState.Loading) assertTrue(emissions[1] is DataState.Success) + verify { mockJob.status = ConnectJobRecord.STATUS_DELIVERING } + verify { ConnectJobUtils.upsertJob(mockJob) } } @Test @@ -323,13 +326,18 @@ class ConnectRepositoryTest { @Test fun testConfirmPayments_success_emitsLoadingThenSuccess() = runBlocking { + val paymentRecord = mockk(relaxed = true) + val paymentConfirmation = ConnectPaymentConfirmationModel(paymentRecord, toConfirm = true) coEvery { mockNetworkClient.confirmPayments(any(), any()) } returns Result.success(Unit) + every { ConnectJobUtils.storePayment(any(), any()) } just Runs - val emissions = repository.confirmPayments(emptyList()).toList() + val emissions = repository.confirmPayments(listOf(paymentConfirmation)).toList() assertEquals(2, emissions.size) assertTrue(emissions[0] is DataState.Loading) assertTrue(emissions[1] is DataState.Success) + verify { paymentRecord.confirmed = true } + verify { ConnectJobUtils.storePayment(any(), paymentRecord) } } @Test diff --git a/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt b/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt index 7170e30033..0c34e967df 100644 --- a/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt +++ b/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt @@ -13,7 +13,6 @@ import org.commcare.connect.repository.DataState import org.commcare.rules.MainCoroutineRule import org.junit.After import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -56,7 +55,7 @@ class ConnectJobIntroViewModelTest { assertEquals(2, results.size) assertEquals(DataState.Loading, results[0]) - assertTrue(results[1] is DataState.Success) + assertEquals(DataState.Success(Unit), results[1]) } @Test @@ -72,6 +71,6 @@ class ConnectJobIntroViewModelTest { } assertEquals(2, results.size) - assertTrue(results[1] is DataState.Error) + assertEquals(DataState.Error(), results[1]) } } From 1b2be3e513042bf6b492d10da46c83302a579b3c Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 18 Aug 2026 18:41:22 +0530 Subject: [PATCH 8/9] natural language for test names --- .../connect/viewmodel/ConnectJobIntroViewModelTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt b/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt index 0c34e967df..375b56df05 100644 --- a/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt +++ b/app/unit-tests/src/org/commcare/connect/viewmodel/ConnectJobIntroViewModelTest.kt @@ -42,7 +42,7 @@ class ConnectJobIntroViewModelTest { } @Test - fun testStartLearning_postsLoadingThenSuccess() { + fun `startLearning posts loading then success`() { every { mockRepository.startLearning(any()) } returns flowOf(DataState.Loading, DataState.Success(Unit)) @@ -59,7 +59,7 @@ class ConnectJobIntroViewModelTest { } @Test - fun testStartLearning_postsError_onFailure() { + fun `startLearning posts error on failure`() { every { mockRepository.startLearning(any()) } returns flowOf(DataState.Loading, DataState.Error()) From 95f1bfe82a7c9f15457f0bb4a96dee66e8199b3d Mon Sep 17 00:00:00 2001 From: Shubham Goyal Date: Tue, 18 Aug 2026 19:55:12 +0530 Subject: [PATCH 9/9] Merge ConnectJobHelper with ConnectJobUtils --- .../commcare/activities/DispatchActivity.java | 4 +- .../activities/HomeScreenBaseActivity.java | 4 +- .../activities/StandardHomeActivity.java | 4 +- .../StandardHomeActivityUIController.java | 3 +- .../activities/connect/ConnectActivity.java | 3 +- .../org/commcare/connect/ConnectJobHelper.kt | 59 ------------------- .../connect/database/ConnectJobUtils.java | 50 ++++++++++++++++ .../connect/database/ConnectTaskUtils.kt | 4 +- .../connect/ConnectUnlockFragment.java | 3 +- .../connect/database/ConnectTaskUtilsTest.kt | 22 ++++--- 10 files changed, 71 insertions(+), 85 deletions(-) delete mode 100644 app/src/org/commcare/connect/ConnectJobHelper.kt diff --git a/app/src/org/commcare/activities/DispatchActivity.java b/app/src/org/commcare/activities/DispatchActivity.java index 45bb17107e..38814aa472 100644 --- a/app/src/org/commcare/activities/DispatchActivity.java +++ b/app/src/org/commcare/activities/DispatchActivity.java @@ -13,7 +13,7 @@ import org.commcare.android.database.connect.models.ConnectJobRecord; import org.commcare.android.database.global.models.ApplicationRecord; import org.commcare.android.database.user.models.SessionStateDescriptor; -import org.commcare.connect.ConnectJobHelper; +import org.commcare.connect.database.ConnectJobUtils; import org.commcare.connect.ConnectNavHelper; import org.commcare.connect.utils.DeepLinkHelper; import org.commcare.dalvik.R; @@ -247,7 +247,7 @@ private void dispatch() { handleShortcutLaunch(); } else if (redirectToConnectOpportunityInfo) { redirectToConnectOpportunityInfo = false; - ConnectJobRecord job = ConnectJobHelper.INSTANCE.getJobForSeatedApp(this); + ConnectJobRecord job = ConnectJobUtils.getJobForSeatedApp(this); ConnectNavHelper.INSTANCE.goToActiveInfoForJob(this, job, true); } else { launchHomeScreen(); diff --git a/app/src/org/commcare/activities/HomeScreenBaseActivity.java b/app/src/org/commcare/activities/HomeScreenBaseActivity.java index 9305f59e30..4e13c0a620 100644 --- a/app/src/org/commcare/activities/HomeScreenBaseActivity.java +++ b/app/src/org/commcare/activities/HomeScreenBaseActivity.java @@ -29,7 +29,7 @@ import org.commcare.appupdate.AppUpdateControllerFactory; import org.commcare.appupdate.AppUpdateState; import org.commcare.appupdate.FlexibleAppUpdateController; -import org.commcare.connect.ConnectJobHelper; +import org.commcare.connect.database.ConnectJobUtils; import org.commcare.connect.ConnectNavHelper; import org.commcare.core.process.CommCareInstanceInitializer; import org.commcare.dalvik.BuildConfig; @@ -632,7 +632,7 @@ protected void userPressedOpportunityStatus() { // Launch the seated app's job status page on top of this (still-live) Home so the app // session is preserved and backing out of the status page returns here. ConnectJobRecord job = Objects.requireNonNull( - ConnectJobHelper.INSTANCE.getJobForSeatedApp(this), + ConnectJobUtils.getJobForSeatedApp(this), "View Job Status pressed but no Connect job was found for the seated app" ); ConnectNavHelper.INSTANCE.goToActiveInfoForJob(this, job, true); diff --git a/app/src/org/commcare/activities/StandardHomeActivity.java b/app/src/org/commcare/activities/StandardHomeActivity.java index 6cd3c42fc5..c142b714e7 100755 --- a/app/src/org/commcare/activities/StandardHomeActivity.java +++ b/app/src/org/commcare/activities/StandardHomeActivity.java @@ -11,7 +11,7 @@ import org.commcare.CommCareApplication; import org.commcare.CommCareNoficationManager; -import org.commcare.connect.ConnectJobHelper; +import org.commcare.connect.database.ConnectJobUtils; import org.commcare.android.database.connect.models.ConnectJobRecord; import org.commcare.connect.repository.ConnectRepository; import org.commcare.connect.ConnectNavHelper; @@ -379,6 +379,6 @@ public void fetchJobProgressOverNetwork() { } public ConnectJobRecord getActiveJob() { - return ConnectJobHelper.INSTANCE.getJobForSeatedApp(this); + return ConnectJobUtils.getJobForSeatedApp(this); } } diff --git a/app/src/org/commcare/activities/StandardHomeActivityUIController.java b/app/src/org/commcare/activities/StandardHomeActivityUIController.java index c43b3a721d..499e8d8a43 100644 --- a/app/src/org/commcare/activities/StandardHomeActivityUIController.java +++ b/app/src/org/commcare/activities/StandardHomeActivityUIController.java @@ -25,7 +25,6 @@ import org.commcare.android.database.connect.models.ConnectJobRecord; import org.commcare.android.database.connect.models.ConnectTaskRecord; import org.commcare.connect.ConnectDateUtils; -import org.commcare.connect.ConnectJobHelper; import org.commcare.connect.ConnectNavHelper; import org.commcare.connect.database.ConnectJobUtils; import org.commcare.connect.database.ConnectTaskUtils; @@ -262,7 +261,7 @@ private Vector getHiddenButtons() { if (!CommCareApplication.instance().getCurrentApp().hasVisibleTrainingContent()) { hiddenButtons.add("training"); } - if (!ConnectJobHelper.INSTANCE.shouldShowJobStatus(activity, ccApp.getUniqueId())) { + if (!ConnectJobUtils.shouldShowJobStatus(activity, ccApp.getUniqueId())) { hiddenButtons.add("connect"); } return hiddenButtons; diff --git a/app/src/org/commcare/activities/connect/ConnectActivity.java b/app/src/org/commcare/activities/connect/ConnectActivity.java index e24558a0f8..4d7d51a532 100644 --- a/app/src/org/commcare/activities/connect/ConnectActivity.java +++ b/app/src/org/commcare/activities/connect/ConnectActivity.java @@ -27,7 +27,6 @@ import org.commcare.activities.NavigationHostCommCareActivity; import org.commcare.connect.ConnectConstants; import org.commcare.android.database.connect.models.ConnectJobRecord; -import org.commcare.connect.ConnectJobHelper; import org.commcare.connect.ConnectNavHelper; import org.commcare.connect.MessageManager; import org.commcare.connect.PersonalIdManager; @@ -161,7 +160,7 @@ private int handleSecureRedirect(Bundle startArgs) { ); } - redirectionAction = ConnectJobHelper.INSTANCE.resolveGenericOpportunityDestination( + redirectionAction = ConnectJobUtils.resolveGenericOpportunityDestination( redirectionAction, job, getIntent().getStringExtra(PAYMENT_UUID)); diff --git a/app/src/org/commcare/connect/ConnectJobHelper.kt b/app/src/org/commcare/connect/ConnectJobHelper.kt deleted file mode 100644 index 149a55ae45..0000000000 --- a/app/src/org/commcare/connect/ConnectJobHelper.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.commcare.connect - -import android.content.Context -import org.commcare.CommCareApplication -import org.commcare.android.database.connect.models.ConnectJobRecord -import org.commcare.connect.database.ConnectJobUtils - -object ConnectJobHelper { - fun getJobForSeatedApp(context: Context): ConnectJobRecord? { - val appId = CommCareApplication.instance().currentApp.uniqueId - val appRecord = ConnectJobUtils.getAppRecord(context, appId) ?: return null - - return ConnectJobUtils.getCompositeJob(context, appRecord.jobUUID) - } - - fun shouldShowJobStatus( - context: Context?, - appId: String?, - ): Boolean { - val record = ConnectJobUtils.getAppRecord(context, appId) ?: return false - val job = ConnectJobUtils.getJobForApp(context, appId) ?: return false - - // Only time not to show is when we're in learn app but job is in delivery state - return !record.isLearning || job.status != ConnectJobRecord.STATUS_DELIVERING - } - - fun resolveGenericOpportunityDestination( - currentAction: String?, - job: ConnectJobRecord?, - paymentUuid: String?, - ): String? { - if (ConnectConstants.CCC_GENERIC_OPPORTUNITY != currentAction || job == null) { - return currentAction - } - return when (job.status) { - ConnectJobRecord.STATUS_DELIVERING -> { - if (!paymentUuid.isNullOrEmpty()) { - ConnectConstants.CCC_DEST_PAYMENTS - } else { - ConnectConstants.CCC_DEST_DELIVERY_PROGRESS - } - } - - ConnectJobRecord.STATUS_LEARNING -> { - ConnectConstants.CCC_DEST_LEARN_PROGRESS - } - - ConnectJobRecord.STATUS_AVAILABLE, - ConnectJobRecord.STATUS_AVAILABLE_NEW, - -> { - ConnectConstants.CCC_DEST_OPPORTUNITY_SUMMARY_PAGE - } - - else -> { - currentAction - } - } - } -} diff --git a/app/src/org/commcare/connect/database/ConnectJobUtils.java b/app/src/org/commcare/connect/database/ConnectJobUtils.java index 4a4327fd62..0e6ad423e0 100644 --- a/app/src/org/commcare/connect/database/ConnectJobUtils.java +++ b/app/src/org/commcare/connect/database/ConnectJobUtils.java @@ -1,6 +1,11 @@ package org.commcare.connect.database; import static org.commcare.connect.ConnectConstants.OPPORTUNITY_STATUS_LEARN; +import static org.commcare.connect.ConnectConstants.CCC_GENERIC_OPPORTUNITY; +import static org.commcare.connect.ConnectConstants.CCC_DEST_PAYMENTS; +import static org.commcare.connect.ConnectConstants.CCC_DEST_DELIVERY_PROGRESS; +import static org.commcare.connect.ConnectConstants.CCC_DEST_LEARN_PROGRESS; +import static org.commcare.connect.ConnectConstants.CCC_DEST_OPPORTUNITY_SUMMARY_PAGE; import android.content.Context; import android.os.Build; @@ -577,4 +582,49 @@ public static boolean isExpiryDateUnderFiveDays(Date expiryDate) { upperBound.add(Calendar.DAY_OF_YEAR, 5); return !expiry.before(today) && !expiry.after(upperBound); } + + public static ConnectJobRecord getJobForSeatedApp(Context context) { + String appId = CommCareApplication.instance().getCurrentApp().getUniqueId(); + ConnectAppRecord appRecord = getAppRecord(context, appId); + if (appRecord == null) { + return null; + } + return getCompositeJob(context, appRecord.getJobUUID()); + } + + public static boolean shouldShowJobStatus(Context context, String appId) { + ConnectAppRecord record = getAppRecord(context, appId); + if (record == null) { + return false; + } + ConnectJobRecord job = getJobForApp(context, appId); + if (job == null) { + return false; + } + // Only time not to show is when we're in learn app but job is in delivery state + return !record.getIsLearning() || job.getStatus() != ConnectJobRecord.STATUS_DELIVERING; + } + + public static String resolveGenericOpportunityDestination( + String currentAction, + ConnectJobRecord job, + String paymentUuid + ) { + if (!CCC_GENERIC_OPPORTUNITY.equals(currentAction) || job == null) { + return currentAction; + } + int status = job.getStatus(); + if (status == ConnectJobRecord.STATUS_DELIVERING) { + return (paymentUuid != null && !paymentUuid.isEmpty()) + ? CCC_DEST_PAYMENTS + : CCC_DEST_DELIVERY_PROGRESS; + } else if (status == ConnectJobRecord.STATUS_LEARNING) { + return CCC_DEST_LEARN_PROGRESS; + } else if (status == ConnectJobRecord.STATUS_AVAILABLE + || status == ConnectJobRecord.STATUS_AVAILABLE_NEW) { + return CCC_DEST_OPPORTUNITY_SUMMARY_PAGE; + } else { + return currentAction; + } + } } diff --git a/app/src/org/commcare/connect/database/ConnectTaskUtils.kt b/app/src/org/commcare/connect/database/ConnectTaskUtils.kt index 6b841b3a41..9e0c777d67 100644 --- a/app/src/org/commcare/connect/database/ConnectTaskUtils.kt +++ b/app/src/org/commcare/connect/database/ConnectTaskUtils.kt @@ -3,7 +3,7 @@ package org.commcare.connect.database import android.content.Context import org.commcare.android.database.connect.models.ConnectJobRecord import org.commcare.android.database.connect.models.ConnectTaskRecord -import org.commcare.connect.ConnectJobHelper.getJobForSeatedApp +import org.commcare.connect.database.ConnectJobUtils import org.commcare.models.database.SqlStorage import org.commcare.preferences.ConnectJobPreferences import org.commcare.utils.SyncDetailCalculations @@ -120,7 +120,7 @@ object ConnectTaskUtils { @JvmStatic fun isLastTaskUpdateLaterThanLastSync(context: Context): Boolean { - val job = getJobForSeatedApp(context) + val job = ConnectJobUtils.getJobForSeatedApp(context) if (job == null || job.status != ConnectJobRecord.STATUS_DELIVERING) { return false } diff --git a/app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java b/app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java index 74cca25938..ec79084793 100644 --- a/app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java +++ b/app/src/org/commcare/fragments/connect/ConnectUnlockFragment.java @@ -22,7 +22,6 @@ import org.commcare.activities.connect.ConnectActivity; import org.commcare.android.database.connect.models.ConnectJobRecord; import org.commcare.connect.ConnectConstants; -import org.commcare.connect.ConnectJobHelper; import org.commcare.connect.repository.ConnectRepository; import org.commcare.connect.database.ConnectJobUtils; import org.commcare.connect.database.ConnectUserDatabaseUtil; @@ -115,7 +114,7 @@ private void tryToLoadInvitedOpp(boolean refreshSucceeded) { FirebaseAnalyticsUtil.reportExternalAppLaunchEvent( getOppInviteSource(), true, null); ((ConnectActivity) requireActivity()).setActiveJob(requested); - redirectionAction = ConnectJobHelper.INSTANCE.resolveGenericOpportunityDestination( + redirectionAction = ConnectJobUtils.resolveGenericOpportunityDestination( redirectionAction, requested, null); } } diff --git a/app/unit-tests/src/org/commcare/connect/database/ConnectTaskUtilsTest.kt b/app/unit-tests/src/org/commcare/connect/database/ConnectTaskUtilsTest.kt index 0e1fb27d03..2527dce5ca 100644 --- a/app/unit-tests/src/org/commcare/connect/database/ConnectTaskUtilsTest.kt +++ b/app/unit-tests/src/org/commcare/connect/database/ConnectTaskUtilsTest.kt @@ -5,7 +5,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.every import io.mockk.mockk -import io.mockk.mockkObject import io.mockk.mockkStatic import io.mockk.unmockkAll import org.commcare.CommCareTestApplication @@ -14,7 +13,6 @@ import org.commcare.android.database.connect.models.ConnectJobRecord.STATUS_DELI import org.commcare.android.database.connect.models.ConnectTaskRecord import org.commcare.android.database.connect.models.ConnectTaskRecord.Companion.STATUS_ASSIGNED import org.commcare.android.database.connect.models.ConnectTaskRecord.Companion.STATUS_COMPLETED -import org.commcare.connect.ConnectJobHelper import org.commcare.preferences.ConnectJobPreferences import org.commcare.utils.SyncDetailCalculations import org.junit.After @@ -325,24 +323,24 @@ class ConnectTaskUtilsTest { @Test fun `isLastTaskUpdateLaterThanLastSync returns false when no job is seated`() { - mockkObject(ConnectJobHelper) - every { ConnectJobHelper.getJobForSeatedApp(context) } returns null + mockkStatic(ConnectJobUtils::class) + every { ConnectJobUtils.getJobForSeatedApp(context) } returns null assertFalse(ConnectTaskUtils.isLastTaskUpdateLaterThanLastSync(context)) } @Test fun `isLastTaskUpdateLaterThanLastSync returns false when job is not DELIVERING`() { - mockkObject(ConnectJobHelper) - every { ConnectJobHelper.getJobForSeatedApp(context) } returns makeJob(ConnectJobRecord.STATUS_LEARNING) + mockkStatic(ConnectJobUtils::class) + every { ConnectJobUtils.getJobForSeatedApp(context) } returns makeJob(ConnectJobRecord.STATUS_LEARNING) assertFalse(ConnectTaskUtils.isLastTaskUpdateLaterThanLastSync(context)) } @Test fun `isLastTaskUpdateLaterThanLastSync returns false when task modified time is not set`() { - mockkObject(ConnectJobHelper) - every { ConnectJobHelper.getJobForSeatedApp(context) } returns makeJob() + mockkStatic(ConnectJobUtils::class) + every { ConnectJobUtils.getJobForSeatedApp(context) } returns makeJob() assertFalse(ConnectTaskUtils.isLastTaskUpdateLaterThanLastSync(context)) } @@ -354,9 +352,9 @@ class ConnectTaskUtilsTest { ConnectTaskUtils.storeTasks(context, listOf(makeTask(taskId = "task-1", status = STATUS_COMPLETED)), jobUUID) // Mock last sync as happening after the task update - mockkObject(ConnectJobHelper) + mockkStatic(ConnectJobUtils::class) mockkStatic(SyncDetailCalculations::class) - every { ConnectJobHelper.getJobForSeatedApp(context) } returns makeJob() + every { ConnectJobUtils.getJobForSeatedApp(context) } returns makeJob() every { SyncDetailCalculations.getLastSyncTime() } returns System.currentTimeMillis() + 10_000L assertFalse(ConnectTaskUtils.isLastTaskUpdateLaterThanLastSync(context)) @@ -364,9 +362,9 @@ class ConnectTaskUtilsTest { @Test fun `isLastTaskUpdateLaterThanLastSync returns true when task was modified after last sync`() { - mockkObject(ConnectJobHelper) + mockkStatic(ConnectJobUtils::class) mockkStatic(SyncDetailCalculations::class) - every { ConnectJobHelper.getJobForSeatedApp(context) } returns makeJob() + every { ConnectJobUtils.getJobForSeatedApp(context) } returns makeJob() every { SyncDetailCalculations.getLastSyncTime() } returns 0L seedTask(makeTask(taskId = "task-1", status = STATUS_ASSIGNED)) ConnectTaskUtils.storeTasks(context, listOf(makeTask(taskId = "task-1", status = STATUS_COMPLETED)), jobUUID)