diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index bff939a57faf..3d99876fa4f1 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -1307,7 +1307,7 @@ class BrowserTabViewModelTest { } @Test - fun whenDuckDuckGoUrlContainingQueryLoadedThenAtbRefreshed() { + fun whenDuckDuckGoUrlContainingQueryLoadedThenAtbRefreshed() = runTest { loadUrl("http://duckduckgo.com?q=test") verify(mockStatisticsUpdater).refreshSearchRetentionAtb() } @@ -3489,7 +3489,7 @@ class BrowserTabViewModelTest { } @Test - fun whenConsumeAliasAndCopyToClipboardThenCopyAliasToClipboardCommandSent() { + fun whenConsumeAliasAndCopyToClipboardThenCopyAliasToClipboardCommandSent() = runTest { whenever(mockEmailManager.getAlias()).thenReturn("alias") testee.consumeAliasAndCopyToClipboard() @@ -3498,7 +3498,7 @@ class BrowserTabViewModelTest { } @Test - fun whenConsumeAliasAndCopyToClipboardThenSetNewLastUsedDateCalled() { + fun whenConsumeAliasAndCopyToClipboardThenSetNewLastUsedDateCalled() = runTest { whenever(mockEmailManager.getAlias()).thenReturn("alias") testee.consumeAliasAndCopyToClipboard() @@ -3507,7 +3507,7 @@ class BrowserTabViewModelTest { } @Test - fun whenConsumeAliasAndCopyToClipboardThenPixelSent() { + fun whenConsumeAliasAndCopyToClipboardThenPixelSent() = runTest { whenever(mockEmailManager.getAlias()).thenReturn("alias") whenever(mockEmailManager.getCohort()).thenReturn("cohort") whenever(mockEmailManager.getLastUsedDate()).thenReturn("2021-01-01") @@ -3550,7 +3550,7 @@ class BrowserTabViewModelTest { } @Test - fun whenConsumeAliasThenInjectAddressCommandSent() { + fun whenConsumeAliasThenInjectAddressCommandSent() = runTest { whenever(mockEmailManager.getAlias()).thenReturn("alias") testee.usePrivateDuckAddress("", "alias") @@ -3561,7 +3561,7 @@ class BrowserTabViewModelTest { } @Test - fun whenUseAddressThenInjectAddressCommandSent() { + fun whenUseAddressThenInjectAddressCommandSent() = runTest { whenever(mockEmailManager.getEmailAddress()).thenReturn("address") testee.usePersonalDuckAddress("", "address") @@ -3572,7 +3572,7 @@ class BrowserTabViewModelTest { } @Test - fun whenShowEmailTooltipIfAddressExistsThenShowEmailTooltipCommandSent() { + fun whenShowEmailTooltipIfAddressExistsThenShowEmailTooltipCommandSent() = runTest { whenever(mockEmailManager.getEmailAddress()).thenReturn("address") testee.showEmailProtectionChooseEmailPrompt() @@ -3583,7 +3583,7 @@ class BrowserTabViewModelTest { } @Test - fun whenShowEmailTooltipIfAddressDoesNotExistThenCommandNotSent() { + fun whenShowEmailTooltipIfAddressDoesNotExistThenCommandNotSent() = runTest { whenever(mockEmailManager.getEmailAddress()).thenReturn(null) testee.showEmailProtectionChooseEmailPrompt() diff --git a/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt b/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt index fd5ff2405b0f..e419035a7f5b 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/di/StubStatisticsModule.kt @@ -58,13 +58,13 @@ class StubStatisticsModule { fun stubStatisticsUpdater(): StatisticsUpdater { return object : StatisticsUpdater { - override fun initializeAtb() { + override suspend fun initializeAtb() { } - override fun refreshAppRetentionAtb() { + override suspend fun refreshAppRetentionAtb() { } - override fun refreshSearchRetentionAtb() { + override suspend fun refreshSearchRetentionAtb() { } } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt b/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt index a613d52eb799..82e6ba4659bd 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/email/EmailInjectorJsTest.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.email import android.annotation.SuppressLint import android.webkit.WebView import androidx.test.annotation.UiThreadTest +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SdkSuppress import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.autofill.DefaultEmailProtectionJavascriptInjector @@ -28,14 +29,19 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.autofill.api.Autofill import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.email.EmailManager +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.Toggle import java.io.BufferedReader +import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.* +@RunWith(AndroidJUnit4::class) class EmailInjectorJsTest { private val mockEmailManager: EmailManager = mock() @@ -46,6 +52,9 @@ class EmailInjectorJsTest { lateinit var testee: EmailInjectorJs + @get:Rule + var coroutineRule = CoroutineTestRule() + @Before fun setup() { testee = @@ -56,6 +65,7 @@ class EmailInjectorJsTest { autofillFeature, javascriptInjector, mockAutofill, + coroutineRule.testScope, ) whenever(mockAutofill.isAnException(any())).thenReturn(false) } @@ -107,7 +117,7 @@ class EmailInjectorJsTest { @UiThreadTest @Test @SdkSuppress(minSdkVersion = 24) - fun whenNotifyWebAppSignEventAndUrlIsNotFromDuckDuckGoAndEmailIsSignedInThenDoNotEvaluateJsCode() { + fun whenNotifyWebAppSignEventAndUrlIsNotFromDuckDuckGoAndEmailIsSignedInThenDoNotEvaluateJsCode() = runTest { whenever(mockEmailManager.isSignedIn()).thenReturn(true) val jsToEvaluate = getNotifySignOutJsToEvaluate() val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) @@ -120,7 +130,7 @@ class EmailInjectorJsTest { @UiThreadTest @Test @SdkSuppress(minSdkVersion = 24) - fun whenNotifyWebAppSignEventAndUrlIsNotFromDuckDuckGoAndEmailIsNotSignedInThenDoNotEvaluateJsCode() { + fun whenNotifyWebAppSignEventAndUrlIsNotFromDuckDuckGoAndEmailIsNotSignedInThenDoNotEvaluateJsCode() = runTest { whenever(mockEmailManager.isSignedIn()).thenReturn(false) val jsToEvaluate = getNotifySignOutJsToEvaluate() val webView = spy(WebView(InstrumentationRegistry.getInstrumentation().targetContext)) @@ -134,7 +144,7 @@ class EmailInjectorJsTest { @UiThreadTest @Test @SdkSuppress(minSdkVersion = 24) - fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsDisabledAndEmailIsNotSignedInThenDoNotEvaluateJsCode() { + fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsDisabledAndEmailIsNotSignedInThenDoNotEvaluateJsCode() = runTest { whenever(mockEmailManager.isSignedIn()).thenReturn(false) autofillFeature.self().setRawStoredState(Toggle.State(enable = false)) @@ -150,7 +160,7 @@ class EmailInjectorJsTest { @UiThreadTest @Test @SdkSuppress(minSdkVersion = 24) - fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsEnabledAndEmailIsNotSignedInThenEvaluateJsCode() { + fun whenNotifyWebAppSignEventAndUrlIsFromDuckDuckGoAndFeatureIsEnabledAndEmailIsNotSignedInThenEvaluateJsCode() = runTest { whenever(mockEmailManager.isSignedIn()).thenReturn(false) autofillFeature.self().setRawStoredState(Toggle.State(enable = true)) diff --git a/app/src/androidTest/java/com/duckduckgo/app/email/db/EmailEncryptedSharedPreferencesTest.kt b/app/src/androidTest/java/com/duckduckgo/app/email/db/EmailEncryptedSharedPreferencesTest.kt index 65eb5c88f538..092531d5b678 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/email/db/EmailEncryptedSharedPreferencesTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/email/db/EmailEncryptedSharedPreferencesTest.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.email.db import androidx.test.platform.app.InstrumentationRegistry import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.CoroutineTestRule +import javax.inject.Provider import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals @@ -36,23 +37,34 @@ class EmailEncryptedSharedPreferencesTest { private val mockPixel: Pixel = mock() lateinit var testee: EmailEncryptedSharedPreferences + private val createPreferencesAsyncProvider = object : Provider { + override fun get(): Boolean { + return true + } + } @Before fun before() { - testee = EmailEncryptedSharedPreferences(InstrumentationRegistry.getInstrumentation().targetContext, mockPixel) + testee = EmailEncryptedSharedPreferences( + InstrumentationRegistry.getInstrumentation().targetContext, + mockPixel, + coroutineRule.testScope, + coroutineRule.testDispatcherProvider, + createPreferencesAsyncProvider, + ) } @Test fun whenNextAliasEqualsValueThenValueIsSentToNextAliasChannel() = runTest { - testee.nextAlias = "test" + testee.setNextAlias("test") - assertEquals("test", testee.nextAlias) + assertEquals("test", testee.getNextAlias()) } @Test fun whenNextAliasEqualsNullThenNullIsSentToNextAliasChannel() = runTest { - testee.nextAlias = null + testee.setNextAlias(null) - assertNull(testee.nextAlias) + assertNull(testee.getNextAlias()) } } diff --git a/app/src/androidTest/java/com/duckduckgo/app/integration/AtbIntegrationTest.kt b/app/src/androidTest/java/com/duckduckgo/app/integration/AtbIntegrationTest.kt index ba4c4980be94..53e0aafb87b8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/integration/AtbIntegrationTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/integration/AtbIntegrationTest.kt @@ -31,6 +31,7 @@ import com.duckduckgo.common.test.InstantSchedulersRule import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.experiments.api.VariantManager import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before import org.junit.Rule @@ -85,7 +86,7 @@ class AtbIntegrationTest { } @Test - fun whenNoStatisticsStoredThenAtbInitializationSuccessfullyStoresAtb() { + fun whenNoStatisticsStoredThenAtbInitializationSuccessfullyStoresAtb() = runTest { testee.initializeAtb() assertTrue(statisticsStore.hasInstallationStatistics) val atb = statisticsStore.atb @@ -94,7 +95,7 @@ class AtbIntegrationTest { } @Test - fun whenStatisticsAlreadyStoredThenRefreshSearchSuccessfullyUpdatesSearchRetentionAtbOnly() { + fun whenStatisticsAlreadyStoredThenRefreshSearchSuccessfullyUpdatesSearchRetentionAtbOnly() = runTest { statisticsStore.saveAtb(Atb("v100-1")) assertTrue(statisticsStore.hasInstallationStatistics) @@ -111,7 +112,7 @@ class AtbIntegrationTest { } @Test - fun whenStatisticsAlreadyStoredThenRefreshAppSuccessfullyUpdatesAppRetentionAtbOnly() { + fun whenStatisticsAlreadyStoredThenRefreshAppSuccessfullyUpdatesAppRetentionAtbOnly() = runTest { statisticsStore.saveAtb(Atb("v100-1")) assertTrue(statisticsStore.hasInstallationStatistics) diff --git a/app/src/androidTest/java/com/duckduckgo/app/statistics/api/StatisticsRequesterJsonTest.kt b/app/src/androidTest/java/com/duckduckgo/app/statistics/api/StatisticsRequesterJsonTest.kt index f6c9bc797cd5..25af82241950 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/statistics/api/StatisticsRequesterJsonTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/statistics/api/StatisticsRequesterJsonTest.kt @@ -35,6 +35,7 @@ import java.net.InetSocketAddress import java.net.Proxy import java.util.concurrent.TimeUnit import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -97,7 +98,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshSearchRetentionCallWithUpdateVersionResponseUpdatesAtb() { + fun whenAlreadyInitializedRefreshSearchRetentionCallWithUpdateVersionResponseUpdatesAtb() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_UPDATE_RESPONSE_JSON) testee.refreshSearchRetentionAtb() @@ -105,7 +106,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshAppRetentionCallWithUpdateVersionResponseUpdatesAtb() { + fun whenAlreadyInitializedRefreshAppRetentionCallWithUpdateVersionResponseUpdatesAtb() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_UPDATE_RESPONSE_JSON) testee.refreshAppRetentionAtb() @@ -113,7 +114,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenNotYetInitializedAtbInitializationStoresAtbResponse() { + fun whenNotYetInitializedAtbInitializationStoresAtbResponse() = runTest { queueResponseFromFile(VALID_JSON) queueResponseFromString(responseBody = "", responseCode = 200) testee.initializeAtb() @@ -122,7 +123,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenNotYetInitializedAtbInitializationResultsInStoredStats() { + fun whenNotYetInitializedAtbInitializationResultsInStoredStats() = runTest { queueResponseFromFile(VALID_JSON) queueResponseFromString(responseBody = "", responseCode = 200) testee.initializeAtb() @@ -131,7 +132,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenNotYetInitializedAndAtbInitializationHasMissingRequiredJsonFieldThenNoStatsStored() { + fun whenNotYetInitializedAndAtbInitializationHasMissingRequiredJsonFieldThenNoStatsStored() = runTest { queueResponseFromFile(INVALID_JSON_MISSING_VERSION) testee.initializeAtb() assertFalse(statisticsStore.hasInstallationStatistics) @@ -139,7 +140,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenNotYetInitializedAndAtbInitializationResponseIsCorruptThenNoStatsStored() { + fun whenNotYetInitializedAndAtbInitializationResponseIsCorruptThenNoStatsStored() = runTest { queueResponseFromFile(INVALID_JSON_CORRUPT_JSON) queueResponseFromString(responseBody = "", responseCode = 200) testee.initializeAtb() @@ -148,7 +149,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenNotYetInitializedAndExtiCallErrorsThenNoStatsStored() { + fun whenNotYetInitializedAndExtiCallErrorsThenNoStatsStored() = runTest { queueResponseFromFile(VALID_JSON) queueError() testee.initializeAtb() @@ -157,14 +158,14 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedAtbInitializationDoesNotReInitialize() { + fun whenAlreadyInitializedAtbInitializationDoesNotReInitialize() = runTest { statisticsStore.saveAtb(Atb("123")) testee.initializeAtb() assertNumberRequestsMade(0) } @Test - fun whenNotYetInitializedAtbInitializationRetrievesFromCorrectEndpoint() { + fun whenNotYetInitializedAtbInitializationRetrievesFromCorrectEndpoint() = runTest { queueResponseFromFile(VALID_JSON) queueResponseFromString("", 200) testee.initializeAtb() @@ -173,7 +174,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenNotYetInitializedAtbInitializationSendsTestParameter() { + fun whenNotYetInitializedAtbInitializationSendsTestParameter() = runTest { queueResponseFromFile(VALID_JSON) queueResponseFromString("", 200) testee.initializeAtb() @@ -183,7 +184,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenNotYetInitializedExtiInitializationRetrievesFromCorrectEndpoint() { + fun whenNotYetInitializedExtiInitializationRetrievesFromCorrectEndpoint() = runTest { queueResponseFromFile(VALID_JSON) queueResponseFromString("", 200) testee.initializeAtb() @@ -193,7 +194,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenNotYetInitializedExtiInitializationSendsTestParameter() { + fun whenNotYetInitializedExtiInitializationSendsTestParameter() = runTest { queueResponseFromFile(VALID_JSON) queueResponseFromString("", 200) testee.initializeAtb() @@ -204,7 +205,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenNotYetInitializedExtiInitializationSendsCorrectAtb() { + fun whenNotYetInitializedExtiInitializationSendsCorrectAtb() = runTest { queueResponseFromFile(VALID_JSON) queueResponseFromString("", 200) testee.initializeAtb() @@ -216,7 +217,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshSearchCallGoesToCorrectEndpoint() { + fun whenAlreadyInitializedRefreshSearchCallGoesToCorrectEndpoint() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) testee.refreshSearchRetentionAtb() @@ -226,7 +227,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshAppCallGoesToCorrectEndpoint() { + fun whenAlreadyInitializedRefreshAppCallGoesToCorrectEndpoint() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) testee.refreshAppRetentionAtb() @@ -236,7 +237,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshSearchCallUpdatesSearchRetentionAtb() { + fun whenAlreadyInitializedRefreshSearchCallUpdatesSearchRetentionAtb() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) testee.refreshSearchRetentionAtb() @@ -244,7 +245,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshAppCallUpdatesAppRetentionAtb() { + fun whenAlreadyInitializedRefreshAppCallUpdatesAppRetentionAtb() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) testee.refreshAppRetentionAtb() @@ -252,7 +253,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshSearchCallSendsTestParameter() { + fun whenAlreadyInitializedRefreshSearchCallSendsTestParameter() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) testee.refreshSearchRetentionAtb() @@ -262,7 +263,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshAppCallSendsTestParameter() { + fun whenAlreadyInitializedRefreshAppCallSendsTestParameter() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) testee.refreshAppRetentionAtb() @@ -272,7 +273,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshSearchCallSendsCorrectAtb() { + fun whenAlreadyInitializedRefreshSearchCallSendsCorrectAtb() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) testee.refreshSearchRetentionAtb() @@ -282,7 +283,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshAppCallSendsCorrectAtb() { + fun whenAlreadyInitializedRefreshAppCallSendsCorrectAtb() = runTest { statisticsStore.saveAtb(Atb("100-1")) queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) testee.refreshAppRetentionAtb() @@ -292,7 +293,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshSearchCallSendsCorrectRetentionAtb() { + fun whenAlreadyInitializedRefreshSearchCallSendsCorrectRetentionAtb() = runTest { statisticsStore.saveAtb(Atb("100-1")) statisticsStore.searchRetentionAtb = "101-3" queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) @@ -303,7 +304,7 @@ class StatisticsRequesterJsonTest { } @Test - fun whenAlreadyInitializedRefreshAppCallSendsCorrectRetentionAtb() { + fun whenAlreadyInitializedRefreshAppCallSendsCorrectRetentionAtb() = runTest { statisticsStore.saveAtb(Atb("100-1")) statisticsStore.appRetentionAtb = "101-3" queueResponseFromFile(VALID_REFRESH_RESPONSE_JSON) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 79a1a0fd69e1..0971eefddef2 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1576,7 +1576,9 @@ class BrowserTabViewModel @Inject constructor( ) if (duckDuckGoUrlDetector.isDuckDuckGoQueryUrl(url)) { - statisticsUpdater.refreshSearchRetentionAtb() + viewModelScope.launch { + statisticsUpdater.refreshSearchRetentionAtb() + } } domain?.let { viewModelScope.launch { updateLoadingStatePrivacy(domain) } } @@ -3121,22 +3123,26 @@ class BrowserTabViewModel @Inject constructor( } fun showEmailProtectionChooseEmailPrompt() { - emailManager.getEmailAddress()?.let { - command.postValue(ShowEmailProtectionChooseEmailPrompt(it)) + viewModelScope.launch { + emailManager.getEmailAddress()?.let { + command.postValue(ShowEmailProtectionChooseEmailPrompt(it)) + } } } fun consumeAliasAndCopyToClipboard() { - emailManager.getAlias()?.let { - command.value = CopyAliasToClipboard(it) - pixel.enqueueFire( - AppPixelName.EMAIL_COPIED_TO_CLIPBOARD, - mapOf( - PixelParameter.COHORT to emailManager.getCohort(), - PixelParameter.LAST_USED_DAY to emailManager.getLastUsedDate(), - ), - ) - emailManager.setNewLastUsedDate() + viewModelScope.launch { + emailManager.getAlias()?.let { + command.value = CopyAliasToClipboard(it) + pixel.enqueueFire( + AppPixelName.EMAIL_COPIED_TO_CLIPBOARD, + mapOf( + PixelParameter.COHORT to emailManager.getCohort(), + PixelParameter.LAST_USED_DAY to emailManager.getLastUsedDate(), + ), + ) + emailManager.setNewLastUsedDate() + } } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserDetector.kt index 001d12c65d1e..d29b9e5f6c40 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/defaultbrowsing/DefaultBrowserDetector.kt @@ -67,7 +67,7 @@ class AndroidDefaultBrowserDetector @Inject constructor( return resolutionInfo?.activityInfo?.packageName } - override fun featureStateParams(): Map { + override suspend fun featureStateParams(): Map { return mapOf(PixelParameter.DEFAULT_BROWSER to isDefaultBrowser().toBinaryString()) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarPositionDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarPositionDetector.kt index df332394c22f..f1dad5f3b44c 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarPositionDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarPositionDetector.kt @@ -33,7 +33,7 @@ interface OmnibarPositionReporterPlugin class OmnibarPositionDetector @Inject constructor( private val settingsDataStore: SettingsDataStore, ) : OmnibarPositionReporterPlugin, BrowserFeatureStateReporterPlugin { - override fun featureStateParams(): Map { + override suspend fun featureStateParams(): Map { return mapOf(PixelParameter.ADDRESS_BAR to settingsDataStore.omnibarPosition.name) } } diff --git a/app/src/main/java/com/duckduckgo/app/email/AppEmailManager.kt b/app/src/main/java/com/duckduckgo/app/email/AppEmailManager.kt index b9314698e11e..a05d98425657 100644 --- a/app/src/main/java/com/duckduckgo/app/email/AppEmailManager.kt +++ b/app/src/main/java/com/duckduckgo/app/email/AppEmailManager.kt @@ -38,6 +38,7 @@ import javax.inject.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.json.JSONObject import timber.log.Timber @@ -56,7 +57,7 @@ class AppEmailManager @Inject constructor( private val isSignedInStateFlow = MutableStateFlow(false) override fun signedInFlow(): StateFlow = isSignedInStateFlow.asStateFlow() - override fun getAlias(): String? = consumeAlias() + override suspend fun getAlias(): String? = consumeAlias() init { // first call to isSignedIn() can be expensive and cause ANRs if done on main thread, so we do it on a background thread @@ -68,19 +69,19 @@ class AppEmailManager @Inject constructor( } } - override fun isSignedIn(): Boolean { - return !emailDataStore.emailToken.isNullOrBlank() && !emailDataStore.emailUsername.isNullOrBlank() + override suspend fun isSignedIn(): Boolean { + return !emailDataStore.getEmailToken().isNullOrBlank() && !emailDataStore.getEmailUsername().isNullOrBlank() } - override fun storeCredentials( + override suspend fun storeCredentials( token: String, username: String, cohort: String, ) { - emailDataStore.cohort = cohort - emailDataStore.emailToken = token - emailDataStore.emailUsername = username - appCoroutineScope.launch(dispatcherProvider.io()) { + withContext(dispatcherProvider.io()) { + emailDataStore.setCohort(cohort) + emailDataStore.setEmailToken(token) + emailDataStore.setEmailUsername(username) isSignedInStateFlow.emit(isSignedIn()) generateNewAlias() pixel.fire(EMAIL_ENABLED) @@ -88,8 +89,8 @@ class AppEmailManager @Inject constructor( } } - override fun signOut() { - appCoroutineScope.launch(dispatcherProvider.io()) { + override suspend fun signOut() { + withContext(dispatcherProvider.io()) { emailDataStore.clearEmailData() isSignedInStateFlow.emit(false) pixel.fire(EMAIL_DISABLED) @@ -97,38 +98,38 @@ class AppEmailManager @Inject constructor( } } - override fun getEmailAddress(): String? { - return emailDataStore.emailUsername?.let { + override suspend fun getEmailAddress(): String? { + return emailDataStore.getEmailUsername()?.let { "$it$DUCK_EMAIL_DOMAIN" } } - override fun getUserData(): String { + override suspend fun getUserData(): String { return JSONObject().apply { - put(TOKEN, emailDataStore.emailToken) - put(USERNAME, emailDataStore.emailUsername) - put(NEXT_ALIAS, emailDataStore.nextAlias?.replace(DUCK_EMAIL_DOMAIN, "")) + put(TOKEN, emailDataStore.getEmailToken()) + put(USERNAME, emailDataStore.getEmailUsername()) + put(NEXT_ALIAS, emailDataStore.getNextAlias()?.replace(DUCK_EMAIL_DOMAIN, "")) }.toString() } - override fun getCohort(): String { - val cohort = emailDataStore.cohort + override suspend fun getCohort(): String { + val cohort = emailDataStore.getCohort() return if (cohort.isNullOrBlank()) UNKNOWN_COHORT else cohort } - override fun isEmailFeatureSupported(): Boolean = emailDataStore.canUseEncryption() + override suspend fun isEmailFeatureSupported(): Boolean = emailDataStore.canUseEncryption() - override fun getLastUsedDate(): String = emailDataStore.lastUsedDate.orEmpty() + override suspend fun getLastUsedDate(): String = emailDataStore.getLastUsedDate().orEmpty() - override fun setNewLastUsedDate() { + override suspend fun setNewLastUsedDate() { val formatter: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US).apply { timeZone = TimeZone.getTimeZone("US/Eastern") } val date = formatter.format(Date()) - emailDataStore.lastUsedDate = date + emailDataStore.setLastUsedDate(date) } - override fun getToken(): String? = emailDataStore.emailToken + override suspend fun getToken(): String? = emailDataStore.getEmailToken() private fun refreshEmailState() { Timber.i("Sync-Settings: refreshEmailState()") @@ -142,8 +143,8 @@ class AppEmailManager @Inject constructor( } } - private fun consumeAlias(): String? { - val alias = emailDataStore.nextAlias + private suspend fun consumeAlias(): String? { + val alias = emailDataStore.getNextAlias() emailDataStore.clearNextAlias() appCoroutineScope.launch(dispatcherProvider.io()) { generateNewAlias() @@ -156,15 +157,17 @@ class AppEmailManager @Inject constructor( } private suspend fun fetchAliasFromService() { - emailDataStore.emailToken?.let { token -> + emailDataStore.getEmailToken()?.let { token -> runCatching { emailService.newAlias("Bearer $token") }.onSuccess { alias -> - emailDataStore.nextAlias = if (alias.address.isBlank()) { - null - } else { - "${alias.address}$DUCK_EMAIL_DOMAIN" - } + emailDataStore.setNextAlias( + if (alias.address.isBlank()) { + null + } else { + "${alias.address}$DUCK_EMAIL_DOMAIN" + }, + ) }.onFailure { Timber.w(it, "Failed to fetch alias") } @@ -179,17 +182,17 @@ class AppEmailManager @Inject constructor( const val NEXT_ALIAS = "nextAlias" } - private fun EmailDataStore.clearEmailData() { - emailToken = null - emailUsername = null - nextAlias = null + private suspend fun EmailDataStore.clearEmailData() { + setEmailToken(null) + setEmailUsername(null) + setNextAlias(null) } - private fun EmailDataStore.clearNextAlias() { - nextAlias = null + private suspend fun EmailDataStore.clearNextAlias() { + setNextAlias(null) } - override fun featureStateParams(): Map { + override suspend fun featureStateParams(): Map { return mapOf(PixelParameter.EMAIL to isSignedIn().toBinaryString()) } } diff --git a/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt b/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt index 1999021a1fdf..a85079a2ee15 100644 --- a/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt +++ b/app/src/main/java/com/duckduckgo/app/email/EmailInjectorJs.kt @@ -20,6 +20,7 @@ import android.webkit.WebView import androidx.annotation.UiThread import com.duckduckgo.app.autofill.EmailProtectionJavascriptInjector import com.duckduckgo.app.browser.DuckDuckGoUrlDetector +import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.email.EmailJavascriptInterface.Companion.JAVASCRIPT_INTERFACE_NAME import com.duckduckgo.autofill.api.Autofill import com.duckduckgo.autofill.api.AutofillFeature @@ -29,6 +30,8 @@ import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking @ContributesBinding(AppScope::class) class EmailInjectorJs @Inject constructor( @@ -38,6 +41,7 @@ class EmailInjectorJs @Inject constructor( private val autofillFeature: AutofillFeature, private val emailProtectionJavascriptInjector: EmailProtectionJavascriptInjector, private val autofill: Autofill, + @AppCoroutineScope private val coroutineScope: CoroutineScope, ) : EmailInjector { override fun addJsInterface( @@ -54,6 +58,7 @@ class EmailInjectorJs @Inject constructor( dispatcherProvider, autofillFeature, autofill, + coroutineScope, onSignedInEmailProtectionPromptShown, ), JAVASCRIPT_INTERFACE_NAME, @@ -79,7 +84,7 @@ class EmailInjectorJs @Inject constructor( url: String?, ) { url?.let { - if (isFeatureEnabled() && isDuckDuckGoUrl(url) && !emailManager.isSignedIn()) { + if (isFeatureEnabled() && isDuckDuckGoUrl(url) && runBlocking { !emailManager.isSignedIn() }) { webView.evaluateJavascript("javascript:${emailProtectionJavascriptInjector.getSignOutFunctions(webView.context)}", null) } } diff --git a/app/src/main/java/com/duckduckgo/app/email/EmailJavascriptInterface.kt b/app/src/main/java/com/duckduckgo/app/email/EmailJavascriptInterface.kt index b17215186201..1d5ab6a41a88 100644 --- a/app/src/main/java/com/duckduckgo/app/email/EmailJavascriptInterface.kt +++ b/app/src/main/java/com/duckduckgo/app/email/EmailJavascriptInterface.kt @@ -23,6 +23,8 @@ import com.duckduckgo.autofill.api.Autofill import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.common.utils.DispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.json.JSONObject @@ -33,6 +35,7 @@ class EmailJavascriptInterface( private val dispatcherProvider: DispatcherProvider, private val autofillFeature: AutofillFeature, private val autofill: Autofill, + private val appCoroutineScope: CoroutineScope, private val showNativeTooltip: () -> Unit, ) { @@ -52,7 +55,9 @@ class EmailJavascriptInterface( @JavascriptInterface fun isSignedIn(): String { return if (isUrlFromDuckDuckGoEmail()) { - emailManager.isSignedIn().toString() + runBlocking { + emailManager.isSignedIn().toString() + } } else { "" } @@ -61,7 +66,9 @@ class EmailJavascriptInterface( @JavascriptInterface fun getUserData(): String { return if (isUrlFromDuckDuckGoEmail()) { - emailManager.getUserData() + runBlocking { + emailManager.getUserData() + } } else { "" } @@ -87,14 +94,18 @@ class EmailJavascriptInterface( cohort: String, ) { if (isUrlFromDuckDuckGoEmail()) { - emailManager.storeCredentials(token, username, cohort) + appCoroutineScope.launch { + emailManager.storeCredentials(token, username, cohort) + } } } @JavascriptInterface fun removeCredentials() { if (isUrlFromDuckDuckGoEmail()) { - emailManager.signOut() + appCoroutineScope.launch { + emailManager.signOut() + } } } diff --git a/app/src/main/java/com/duckduckgo/app/email/db/EmailDataStore.kt b/app/src/main/java/com/duckduckgo/app/email/db/EmailDataStore.kt index 68c524ec6a62..a869260d8307 100644 --- a/app/src/main/java/com/duckduckgo/app/email/db/EmailDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/email/db/EmailDataStore.kt @@ -22,10 +22,15 @@ package com.duckduckgo.app.email.db * Provides ability to store and retrieve data related to the duck address feature such as personal username, next alias etc... */ interface EmailDataStore { - var emailToken: String? - var nextAlias: String? - var emailUsername: String? - var cohort: String? - var lastUsedDate: String? - fun canUseEncryption(): Boolean + suspend fun getEmailToken(): String? + suspend fun setEmailToken(value: String?) + suspend fun getNextAlias(): String? + suspend fun setNextAlias(value: String?) + suspend fun getEmailUsername(): String? + suspend fun setEmailUsername(value: String?) + suspend fun getCohort(): String? + suspend fun setCohort(value: String?) + suspend fun getLastUsedDate(): String? + suspend fun setLastUsedDate(value: String?) + suspend fun canUseEncryption(): Boolean } diff --git a/app/src/main/java/com/duckduckgo/app/email/db/EmailEncryptedSharedPreferences.kt.kt b/app/src/main/java/com/duckduckgo/app/email/db/EmailEncryptedSharedPreferences.kt.kt index 0457b6e504e5..49f863766105 100644 --- a/app/src/main/java/com/duckduckgo/app/email/db/EmailEncryptedSharedPreferences.kt.kt +++ b/app/src/main/java/com/duckduckgo/app/email/db/EmailEncryptedSharedPreferences.kt.kt @@ -23,19 +23,54 @@ import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.common.utils.DispatcherProvider import java.io.IOException import java.security.GeneralSecurityException import javax.crypto.AEADBadTagException +import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext class EmailEncryptedSharedPreferences( private val context: Context, private val pixel: Pixel, + private val appCoroutineScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, + private val createPreferencesAsyncProvider: Provider, ) : EmailDataStore { - private val encryptedPreferences: SharedPreferences? by lazy { encryptedPreferences() } + private val mutex: Mutex = Mutex() + private val encryptedPreferencesDeferred: Deferred by lazy { + appCoroutineScope.async(dispatcherProvider.io()) { + encryptedPreferencesAsync() + } + } + + private val encryptedPreferencesSync: SharedPreferences? by lazy { encryptedPreferencesSync() } + private val createPreferencesAsync by lazy { createPreferencesAsyncProvider.get() } + + private suspend fun getEncryptedPreferences(): SharedPreferences? { + return withContext(dispatcherProvider.io()) { + if (createPreferencesAsync) encryptedPreferencesDeferred.await() else encryptedPreferencesSync + } + } + + private suspend fun encryptedPreferencesAsync(): SharedPreferences? { + return mutex.withLock { + innerEncryptedSharedPreferences() + } + } @Synchronized - private fun encryptedPreferences(): SharedPreferences? { + private fun encryptedPreferencesSync(): SharedPreferences? { + return innerEncryptedSharedPreferences() + } + + private fun innerEncryptedSharedPreferences(): SharedPreferences? { try { return EncryptedSharedPreferences.create( context, @@ -64,10 +99,15 @@ class EmailEncryptedSharedPreferences( return null } - override var emailToken: String? - get() = encryptedPreferences?.getString(KEY_EMAIL_TOKEN, null) - set(value) { - encryptedPreferences?.edit(commit = true) { + override suspend fun getEmailToken(): String? { + return withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.getString(KEY_EMAIL_TOKEN, null) + } + } + + override suspend fun setEmailToken(value: String?) { + withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.edit(commit = true) { if (value == null) { remove(KEY_EMAIL_TOKEN) } else { @@ -75,11 +115,17 @@ class EmailEncryptedSharedPreferences( } } } + } + + override suspend fun getNextAlias(): String? { + return withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.getString(KEY_NEXT_ALIAS, null) + } + } - override var nextAlias: String? - get() = encryptedPreferences?.getString(KEY_NEXT_ALIAS, null) - set(value) { - encryptedPreferences?.edit(commit = true) { + override suspend fun setNextAlias(value: String?) { + withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.edit(commit = true) { if (value == null) { remove(KEY_NEXT_ALIAS) } else { @@ -87,11 +133,17 @@ class EmailEncryptedSharedPreferences( } } } + } + + override suspend fun getEmailUsername(): String? { + return withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.getString(KEY_EMAIL_USERNAME, null) + } + } - override var emailUsername: String? - get() = encryptedPreferences?.getString(KEY_EMAIL_USERNAME, null) - set(value) { - encryptedPreferences?.edit(commit = true) { + override suspend fun setEmailUsername(value: String?) { + withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.edit(commit = true) { if (value == null) { remove(KEY_EMAIL_USERNAME) } else { @@ -99,11 +151,17 @@ class EmailEncryptedSharedPreferences( } } } + } - override var cohort: String? - get() = encryptedPreferences?.getString(KEY_COHORT, null) - set(value) { - encryptedPreferences?.edit(commit = true) { + override suspend fun getCohort(): String? { + return withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.getString(KEY_COHORT, null) + } + } + + override suspend fun setCohort(value: String?) { + withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.edit(commit = true) { if (value == null) { remove(KEY_COHORT) } else { @@ -111,11 +169,16 @@ class EmailEncryptedSharedPreferences( } } } + } + override suspend fun getLastUsedDate(): String? { + return withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.getString(KEY_LAST_USED_DATE, null) + } + } - override var lastUsedDate: String? - get() = encryptedPreferences?.getString(KEY_LAST_USED_DATE, null) - set(value) { - encryptedPreferences?.edit(commit = true) { + override suspend fun setLastUsedDate(value: String?) { + withContext(dispatcherProvider.io()) { + getEncryptedPreferences()?.edit(commit = true) { if (value == null) { remove(KEY_LAST_USED_DATE) } else { @@ -123,8 +186,9 @@ class EmailEncryptedSharedPreferences( } } } + } - override fun canUseEncryption(): Boolean = encryptedPreferences != null + override suspend fun canUseEncryption(): Boolean = getEncryptedPreferences() != null private fun canInitialiseEncryptedPreferencesTestFile(): Boolean { return kotlin.runCatching { diff --git a/app/src/main/java/com/duckduckgo/app/email/di/EmailModule.kt b/app/src/main/java/com/duckduckgo/app/email/di/EmailModule.kt index 544f18ec8c44..e3a8ef58970a 100644 --- a/app/src/main/java/com/duckduckgo/app/email/di/EmailModule.kt +++ b/app/src/main/java/com/duckduckgo/app/email/di/EmailModule.kt @@ -17,20 +17,35 @@ package com.duckduckgo.app.email.di import android.content.Context +import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.email.db.EmailDataStore import com.duckduckgo.app.email.db.EmailEncryptedSharedPreferences +import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.common.utils.DispatcherProvider import dagger.Module import dagger.Provides +import javax.inject.Named +import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope @Module class EmailModule { + @Named("asyncEmailPreferencesToggle") + @Provides + fun provideIsAsyncEmailPreferencesToggleEnabled( + androidBrowserConfigFeature: AndroidBrowserConfigFeature, + ): Boolean = androidBrowserConfigFeature.createAsyncEmailPreferences().isEnabled() + @Provides fun providesEmailDataStore( context: Context, pixel: Pixel, + @AppCoroutineScope appCoroutineScope: CoroutineScope, + dispatcherProvider: DispatcherProvider, + @Named("asyncEmailPreferencesToggle") isAsyncEmailPreferencesToggleEnabledProvider: Provider, ): EmailDataStore { - return EmailEncryptedSharedPreferences(context, pixel) + return EmailEncryptedSharedPreferences(context, pixel, appCoroutineScope, dispatcherProvider, isAsyncEmailPreferencesToggleEnabledProvider) } } diff --git a/app/src/main/java/com/duckduckgo/app/email/sync/EmailSync.kt b/app/src/main/java/com/duckduckgo/app/email/sync/EmailSync.kt index c9f3386cff07..9009f4897e60 100644 --- a/app/src/main/java/com/duckduckgo/app/email/sync/EmailSync.kt +++ b/app/src/main/java/com/duckduckgo/app/email/sync/EmailSync.kt @@ -42,9 +42,9 @@ class EmailSync @Inject constructor( override val key: String = DUCK_EMAIL_SETTING - override fun getValue(): String? { - val address = emailDataStore.emailUsername ?: return null - val token = emailDataStore.emailToken ?: return null + override suspend fun getValue(): String? { + val address = emailDataStore.getEmailUsername() ?: return null + val token = emailDataStore.getEmailToken() ?: return null DuckAddressSetting( username = address, personal_access_token = token, @@ -53,7 +53,7 @@ class EmailSync @Inject constructor( } } - override fun save(value: String?): Boolean { + override suspend fun save(value: String?): Boolean { Timber.i("Sync-Settings: save($value)") val duckAddressSetting = runCatching { adapter.fromJson(value) }.getOrNull() if (duckAddressSetting != null) { @@ -67,14 +67,14 @@ class EmailSync @Inject constructor( } } - override fun deduplicate(value: String?): Boolean { + override suspend fun deduplicate(value: String?): Boolean { Timber.i("Sync-Settings: mergeRemote($value)") val duckAddressSetting = runCatching { adapter.fromJson(value) }.getOrNull() if (duckAddressSetting != null) { val duckUsername = duckAddressSetting.username val personalAccessToken = duckAddressSetting.personal_access_token - if (!emailDataStore.emailToken.isNullOrBlank() && !emailDataStore.emailUsername.isNullOrBlank()) { - if (duckUsername != emailDataStore.emailUsername) { + if (!emailDataStore.getEmailToken().isNullOrBlank() && !emailDataStore.getEmailUsername().isNullOrBlank()) { + if (duckUsername != emailDataStore.getEmailUsername()) { storeNewCredentials(duckUsername, personalAccessToken) pixel.fire(AppPixelName.DUCK_EMAIL_OVERRIDE_PIXEL) return true @@ -87,10 +87,10 @@ class EmailSync @Inject constructor( return false } - private fun storeNewCredentials(username: String, token: String) { + private suspend fun storeNewCredentials(username: String, token: String) { Timber.i("Sync-Settings: storeNewCredentials($username, $token)") - emailDataStore.emailToken = token - emailDataStore.emailUsername = username + emailDataStore.setEmailToken(token) + emailDataStore.setEmailUsername(username) listener.invoke() } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt index 2bfcbbac34f7..93229f2cff97 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt @@ -42,7 +42,7 @@ constructor( private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore, ) : ShowOnAppLaunchReporterPlugin, BrowserFeatureStateReporterPlugin { - override fun featureStateParams(): Map { + override suspend fun featureStateParams(): Map { val option = runBlocking(dispatcherProvider.io()) { showOnAppLaunchOptionDataStore.optionFlow.first() diff --git a/app/src/main/java/com/duckduckgo/app/global/store/AndroidUserBrowserProperties.kt b/app/src/main/java/com/duckduckgo/app/global/store/AndroidUserBrowserProperties.kt index 51a79ca6947e..576cc7c42be6 100644 --- a/app/src/main/java/com/duckduckgo/app/global/store/AndroidUserBrowserProperties.kt +++ b/app/src/main/java/com/duckduckgo/app/global/store/AndroidUserBrowserProperties.kt @@ -61,7 +61,7 @@ class AndroidUserBrowserProperties( return appInstallStore.defaultBrowser } - override fun emailEnabled(): Boolean { + override suspend fun emailEnabled(): Boolean { return emailManager.isSignedIn() } diff --git a/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt b/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt index bd03f941d90f..9da4b10df38e 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/remoteconfig/AndroidBrowserConfigFeature.kt @@ -119,4 +119,7 @@ interface AndroidBrowserConfigFeature { */ @Toggle.DefaultValue(DefaultFeatureValue.FALSE) fun omnibarAnimation(): Toggle + + @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + fun createAsyncEmailPreferences(): Toggle } diff --git a/app/src/main/java/com/duckduckgo/app/survey/api/SurveyDownloader.kt b/app/src/main/java/com/duckduckgo/app/survey/api/SurveyDownloader.kt index 648cf2d21181..6184c2c2c6bd 100644 --- a/app/src/main/java/com/duckduckgo/app/survey/api/SurveyDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/survey/api/SurveyDownloader.kt @@ -30,6 +30,7 @@ import java.time.LocalDate import java.time.temporal.ChronoUnit import java.util.* import javax.inject.Inject +import kotlinx.coroutines.runBlocking import retrofit2.Response import timber.log.Timber @@ -87,9 +88,9 @@ class SurveyDownloader @Inject constructor( val newSurvey = when { surveyOption != null -> when { - canSurveyBeScheduled(surveyOption) -> Survey( + runBlocking { canSurveyBeScheduled(surveyOption) } -> Survey( surveyGroup.id, - calculateUrlWithParameters(surveyOption), + runBlocking { calculateUrlWithParameters(surveyOption) }, surveyOption.installationDay, SCHEDULED, ) @@ -107,7 +108,7 @@ class SurveyDownloader @Inject constructor( } } - private fun calculateUrlWithParameters(surveyOption: SurveyOption): String { + private suspend fun calculateUrlWithParameters(surveyOption: SurveyOption): String { val uri = surveyOption.url.toUri() val builder = Uri.Builder() @@ -128,7 +129,7 @@ class SurveyDownloader @Inject constructor( return builder.build().toString() } - private fun canSurveyBeScheduled(surveyOption: SurveyOption): Boolean { + private suspend fun canSurveyBeScheduled(surveyOption: SurveyOption): Boolean { return if (surveyOption.isEmailSignedInRequired == true) { emailManager.isSignedIn() } else if (surveyOption.isNetPOnboardedRequired == true) { diff --git a/app/src/test/java/com/duckduckgo/app/browser/senseofprotection/SenseOfProtectionExperimentImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/senseofprotection/SenseOfProtectionExperimentImplTest.kt index efa0ed2cb4e4..01e27504a292 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/senseofprotection/SenseOfProtectionExperimentImplTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/senseofprotection/SenseOfProtectionExperimentImplTest.kt @@ -356,7 +356,7 @@ class FakeUserBrowserProperties : UserBrowserProperties { TODO("Not yet implemented") } - override fun emailEnabled(): Boolean { + override suspend fun emailEnabled(): Boolean { TODO("Not yet implemented") } diff --git a/app/src/test/java/com/duckduckgo/app/email/AppEmailManagerTest.kt b/app/src/test/java/com/duckduckgo/app/email/AppEmailManagerTest.kt index 2e6ddbd71098..49469d4ee241 100644 --- a/app/src/test/java/com/duckduckgo/app/email/AppEmailManagerTest.kt +++ b/app/src/test/java/com/duckduckgo/app/email/AppEmailManagerTest.kt @@ -76,16 +76,16 @@ class AppEmailManagerTest { @Test fun whenFetchAliasFromServiceThenStoreAliasAddingDuckDomain() = runTest { - mockEmailDataStore.emailToken = "token" + mockEmailDataStore.setEmailToken("token") whenever(mockEmailService.newAlias(any())).thenReturn(EmailAlias("test")) testee.getAlias() - assertEquals("test$DUCK_EMAIL_DOMAIN", mockEmailDataStore.nextAlias) + assertEquals("test$DUCK_EMAIL_DOMAIN", mockEmailDataStore.getNextAlias()) } @Test fun whenFetchAliasFromServiceAndTokenDoesNotExistThenDoNothing() = runTest { - mockEmailDataStore.emailToken = null + mockEmailDataStore.setEmailToken(null) testee.getAlias() verify(mockEmailService, never()).newAlias(any()) @@ -93,11 +93,11 @@ class AppEmailManagerTest { @Test fun whenFetchAliasFromServiceAndAddressIsBlankThenStoreNull() = runTest { - mockEmailDataStore.emailToken = "token" + mockEmailDataStore.setEmailToken("token") whenever(mockEmailService.newAlias(any())).thenReturn(EmailAlias("")) testee.getAlias() - assertNull(mockEmailDataStore.nextAlias) + assertNull(mockEmailDataStore.getNextAlias()) } @Test @@ -108,44 +108,44 @@ class AppEmailManagerTest { } @Test - fun whenGetAliasIfNextAliasDoesNotExistThenReturnNull() { + fun whenGetAliasIfNextAliasDoesNotExistThenReturnNull() = runTest { assertNull(testee.getAlias()) } @Test - fun whenGetAliasThenClearNextAlias() { + fun whenGetAliasThenClearNextAlias() = runTest { testee.getAlias() - assertNull(mockEmailDataStore.nextAlias) + assertNull(mockEmailDataStore.getNextAlias()) } @Test - fun whenIsSignedInAndTokenDoesNotExistThenReturnFalse() { - mockEmailDataStore.emailUsername = "username" - mockEmailDataStore.nextAlias = "alias" + fun whenIsSignedInAndTokenDoesNotExistThenReturnFalse() = runTest { + mockEmailDataStore.setEmailUsername("username") + mockEmailDataStore.setNextAlias("alias") assertFalse(testee.isSignedIn()) } @Test - fun whenIsSignedInAndUsernameDoesNotExistThenReturnFalse() { - mockEmailDataStore.emailToken = "token" - mockEmailDataStore.nextAlias = "alias" + fun whenIsSignedInAndUsernameDoesNotExistThenReturnFalse() = runTest { + mockEmailDataStore.setEmailToken("token") + mockEmailDataStore.setNextAlias("alias") assertFalse(testee.isSignedIn()) } @Test - fun whenIsSignedInAndTokenAndUsernameExistThenReturnTrue() { - mockEmailDataStore.emailToken = "token" - mockEmailDataStore.emailUsername = "username" + fun whenIsSignedInAndTokenAndUsernameExistThenReturnTrue() = runTest { + mockEmailDataStore.setEmailToken("token") + mockEmailDataStore.setEmailUsername("username") assertTrue(testee.isSignedIn()) } @Test fun whenStoreCredentialsThenGenerateNewAlias() = runTest { - mockEmailDataStore.emailToken = "token" + mockEmailDataStore.setEmailToken("token") whenever(mockEmailService.newAlias(any())).thenReturn(EmailAlias("")) testee.storeCredentials("token", "username", "cohort") @@ -155,7 +155,7 @@ class AppEmailManagerTest { @Test fun whenStoreCredentialsThenNotifySyncableSetting() = runTest { - mockEmailDataStore.emailToken = "token" + mockEmailDataStore.setEmailToken("token") whenever(mockEmailService.newAlias(any())).thenReturn(EmailAlias("")) testee.storeCredentials("token", "username", "cohort") @@ -165,7 +165,7 @@ class AppEmailManagerTest { @Test fun whenStoreCredentialsThenSendPixel() = runTest { - mockEmailDataStore.emailToken = "token" + mockEmailDataStore.setEmailToken("token") whenever(mockEmailService.newAlias(any())).thenReturn(EmailAlias("")) testee.storeCredentials("token", "username", "cohort") @@ -174,16 +174,20 @@ class AppEmailManagerTest { } @Test - fun whenStoreCredentialsThenCredentialsAreStoredInDataStore() { + fun whenStoreCredentialsThenCredentialsAreStoredInDataStore() = runTest { + whenever(mockEmailService.newAlias(any())).thenReturn(EmailAlias("")) + testee.storeCredentials("token", "username", "cohort") - assertEquals("username", mockEmailDataStore.emailUsername) - assertEquals("token", mockEmailDataStore.emailToken) - assertEquals("cohort", mockEmailDataStore.cohort) + assertEquals("username", mockEmailDataStore.getEmailUsername()) + assertEquals("token", mockEmailDataStore.getEmailToken()) + assertEquals("cohort", mockEmailDataStore.getCohort()) } @Test fun whenStoreCredentialsIfCredentialsWereCorrectlyStoredThenIsSignedInChannelSendsTrue() = runTest { + whenever(mockEmailService.newAlias(any())).thenReturn(EmailAlias("")) + testee.storeCredentials("token", "username", "cohort") assertTrue(testee.signedInFlow().first()) @@ -191,31 +195,33 @@ class AppEmailManagerTest { @Test fun whenStoreCredentialsIfCredentialsAreBlankThenIsSignedInChannelSendsFalse() = runTest { + whenever(mockEmailService.newAlias(any())).thenReturn(EmailAlias("")) + testee.storeCredentials("", "", "cohort") assertFalse(testee.signedInFlow().first()) } @Test - fun whenSignedOutThenClearEmailDataAndAliasIsNull() { + fun whenSignedOutThenClearEmailDataAndAliasIsNull() = runTest { testee.signOut() - assertNull(mockEmailDataStore.emailUsername) - assertNull(mockEmailDataStore.emailToken) - assertNull(mockEmailDataStore.nextAlias) + assertNull(mockEmailDataStore.getEmailUsername()) + assertNull(mockEmailDataStore.getEmailToken()) + assertNull(mockEmailDataStore.getNextAlias()) assertNull(testee.getAlias()) } @Test - fun whenSignedOutThenNotifySyncableSetting() { + fun whenSignedOutThenNotifySyncableSetting() = runTest { testee.signOut() verify(mockSyncSettingsListener).onSettingChanged(emailSyncableSetting.key) } @Test - fun whenSignedOutThenSendPixel() { + fun whenSignedOutThenSendPixel() = runTest { testee.signOut() verify(mockPixel).fire(EMAIL_DISABLED) @@ -229,69 +235,69 @@ class AppEmailManagerTest { } @Test - fun whenGetEmailAddressThenDuckEmailDomainIsAppended() { - mockEmailDataStore.emailUsername = "username" + fun whenGetEmailAddressThenDuckEmailDomainIsAppended() = runTest { + mockEmailDataStore.setEmailUsername("username") assertEquals("username$DUCK_EMAIL_DOMAIN", testee.getEmailAddress()) } @Test - fun whenGetCohortThenReturnCohort() { - mockEmailDataStore.cohort = "cohort" + fun whenGetCohortThenReturnCohort() = runTest { + mockEmailDataStore.setCohort("cohort") assertEquals("cohort", testee.getCohort()) } @Test - fun whenGetCohortIfCohortIsNullThenReturnUnknown() { - mockEmailDataStore.cohort = null + fun whenGetCohortIfCohortIsNullThenReturnUnknown() = runTest { + mockEmailDataStore.setCohort(null) assertEquals(UNKNOWN_COHORT, testee.getCohort()) } @Test - fun whenGetCohortIfCohortIsEmtpyThenReturnUnknown() { - mockEmailDataStore.cohort = "" + fun whenGetCohortIfCohortIsEmtpyThenReturnUnknown() = runTest { + mockEmailDataStore.setCohort("") assertEquals(UNKNOWN_COHORT, testee.getCohort()) } @Test - fun whenIsEmailFeatureSupportedAndEncryptionCanBeUsedThenReturnTrue() { + fun whenIsEmailFeatureSupportedAndEncryptionCanBeUsedThenReturnTrue() = runTest { (mockEmailDataStore as FakeEmailDataStore).canUseEncryption = true assertTrue(testee.isEmailFeatureSupported()) } @Test - fun whenGetLastUsedDateIfNullThenReturnEmpty() { + fun whenGetLastUsedDateIfNullThenReturnEmpty() = runTest { assertEquals("", testee.getLastUsedDate()) } @Test - fun whenGetLastUsedDateIfNotNullThenReturnValueFromStore() { - mockEmailDataStore.lastUsedDate = "2021-01-01" + fun whenGetLastUsedDateIfNotNullThenReturnValueFromStore() = runTest { + mockEmailDataStore.setLastUsedDate("2021-01-01") assertEquals("2021-01-01", testee.getLastUsedDate()) } @Test - fun whenIsEmailFeatureSupportedAndEncryptionCannotBeUsedThenReturnFalse() { + fun whenIsEmailFeatureSupportedAndEncryptionCannotBeUsedThenReturnFalse() = runTest { (mockEmailDataStore as FakeEmailDataStore).canUseEncryption = false assertFalse(testee.isEmailFeatureSupported()) } @Test - fun whenGetUserDataThenDataReceivedCorrectly() { + fun whenGetUserDataThenDataReceivedCorrectly() = runTest { val expected = JSONObject().apply { put(AppEmailManager.TOKEN, "token") put(AppEmailManager.USERNAME, "user") put(AppEmailManager.NEXT_ALIAS, "nextAlias") }.toString() - mockEmailDataStore.emailToken = "token" - mockEmailDataStore.emailUsername = "user" - mockEmailDataStore.nextAlias = "nextAlias@duck.com" + mockEmailDataStore.setEmailToken("token") + mockEmailDataStore.setEmailUsername("user") + mockEmailDataStore.setNextAlias("nextAlias@duck.com") assertEquals(expected, testee.getUserData()) } @@ -306,8 +312,8 @@ class AppEmailManagerTest { } } - private fun givenNextAliasExists() { - mockEmailDataStore.nextAlias = "alias" + private fun givenNextAliasExists() = runTest { + mockEmailDataStore.setNextAlias("alias") } class TestEmailService : EmailService { @@ -316,12 +322,37 @@ class AppEmailManagerTest { } class FakeEmailDataStore : EmailDataStore { - override var emailToken: String? = null - override var nextAlias: String? = null - override var emailUsername: String? = null - override var cohort: String? = null - override var lastUsedDate: String? = null + + private var _emailToken: String? = null + override suspend fun setEmailToken(value: String?) { + _emailToken = value + } + override suspend fun getEmailToken(): String? = _emailToken + + private var _nextAlias: String? = null + override suspend fun getNextAlias(): String? = _nextAlias + override suspend fun setNextAlias(value: String?) { + _nextAlias = value + } + + private var _emailUsername: String? = null + override suspend fun getEmailUsername(): String? = _emailUsername + override suspend fun setEmailUsername(value: String?) { + _emailUsername = value + } + + private var _cohort: String? = null + override suspend fun getCohort(): String? = _cohort + override suspend fun setCohort(value: String?) { + _cohort = value + } + + private var _lastUsedDate: String? = null + override suspend fun getLastUsedDate(): String? = _lastUsedDate + override suspend fun setLastUsedDate(value: String?) { + _lastUsedDate = value + } var canUseEncryption: Boolean = false - override fun canUseEncryption(): Boolean = canUseEncryption + override suspend fun canUseEncryption(): Boolean = canUseEncryption } diff --git a/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt b/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt index fa1ccb33707c..e1ec878bffd2 100644 --- a/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt +++ b/app/src/test/java/com/duckduckgo/app/email/EmailJavascriptInterfaceTest.kt @@ -25,6 +25,7 @@ import com.duckduckgo.autofill.api.AutofillFeature import com.duckduckgo.autofill.api.email.EmailManager import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.feature.toggles.api.Toggle +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -61,6 +62,7 @@ class EmailJavascriptInterfaceTest { coroutineRule.testDispatcherProvider, autofillFeature, mockAutofill, + coroutineRule.testScope, ) { counter++ } autofillFeature.self().setRawStoredState(Toggle.State(enable = true)) @@ -68,8 +70,9 @@ class EmailJavascriptInterfaceTest { } @Test - fun whenIsSignedInAndUrlIsDuckDuckGoEmailThenIsSignedInCalled() { + fun whenIsSignedInAndUrlIsDuckDuckGoEmailThenIsSignedInCalled() = runTest { whenever(mockWebView.url).thenReturn(DUCKDUCKGO_EMAIL_URL) + whenever(mockEmailManager.isSignedIn()).thenReturn(true) testee.isSignedIn() @@ -77,7 +80,7 @@ class EmailJavascriptInterfaceTest { } @Test - fun whenIsSignedInAndUrlIsNotDuckDuckGoEmailThenIsSignedInNotCalled() { + fun whenIsSignedInAndUrlIsNotDuckDuckGoEmailThenIsSignedInNotCalled() = runTest { whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) testee.isSignedIn() @@ -86,7 +89,7 @@ class EmailJavascriptInterfaceTest { } @Test - fun whenStoreCredentialsAndUrlIsDuckDuckGoEmailThenStoreCredentialsCalledWithCorrectParameters() { + fun whenStoreCredentialsAndUrlIsDuckDuckGoEmailThenStoreCredentialsCalledWithCorrectParameters() = runTest { whenever(mockWebView.url).thenReturn(DUCKDUCKGO_EMAIL_URL) testee.storeCredentials("token", "username", "cohort") @@ -95,7 +98,7 @@ class EmailJavascriptInterfaceTest { } @Test - fun whenStoreCredentialsAndUrlIsNotDuckDuckGoEmailThenStoreCredentialsNotCalled() { + fun whenStoreCredentialsAndUrlIsNotDuckDuckGoEmailThenStoreCredentialsNotCalled() = runTest { whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) testee.storeCredentials("token", "username", "cohort") @@ -104,7 +107,7 @@ class EmailJavascriptInterfaceTest { } @Test - fun whenGetUserDataAndUrlIsDuckDuckGoEmailThenGetUserDataCalled() { + fun whenGetUserDataAndUrlIsDuckDuckGoEmailThenGetUserDataCalled() = runTest { whenever(mockWebView.url).thenReturn(DUCKDUCKGO_EMAIL_URL) testee.getUserData() @@ -113,7 +116,7 @@ class EmailJavascriptInterfaceTest { } @Test - fun whenGetUserDataAndUrlIsNotDuckDuckGoEmailThenGetUserDataIsNotCalled() { + fun whenGetUserDataAndUrlIsNotDuckDuckGoEmailThenGetUserDataIsNotCalled() = runTest { whenever(mockWebView.url).thenReturn(NON_EMAIL_URL) testee.getUserData() diff --git a/app/src/test/java/com/duckduckgo/app/email/EmailSyncTest.kt b/app/src/test/java/com/duckduckgo/app/email/EmailSyncTest.kt index 366e1f493a6c..cb3624b6f880 100644 --- a/app/src/test/java/com/duckduckgo/app/email/EmailSyncTest.kt +++ b/app/src/test/java/com/duckduckgo/app/email/EmailSyncTest.kt @@ -7,6 +7,7 @@ import com.duckduckgo.app.statistics.pixels.* import com.duckduckgo.sync.settings.api.SyncSettingsListener import com.squareup.moshi.* import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import kotlinx.coroutines.test.runTest import org.junit.* import org.junit.Assert.assertEquals import org.junit.Assert.assertNull @@ -21,9 +22,9 @@ class EmailSyncTest { private val testee = EmailSync(emailDataStoreMock, syncSettingsListenerMock, pixelMock) @Test - fun whenUserSignedInThenReturnAccountInfo() { - whenever(emailDataStoreMock.emailUsername).thenReturn("username") - whenever(emailDataStoreMock.emailToken).thenReturn("token") + fun whenUserSignedInThenReturnAccountInfo() = runTest { + whenever(emailDataStoreMock.getEmailUsername()).thenReturn("username") + whenever(emailDataStoreMock.getEmailToken()).thenReturn("token") val value = testee.getValue() @@ -34,9 +35,9 @@ class EmailSyncTest { } @Test - fun whenUserSignedOutThenReturnNull() { - whenever(emailDataStoreMock.emailUsername).thenReturn(null) - whenever(emailDataStoreMock.emailToken).thenReturn(null) + fun whenUserSignedOutThenReturnNull() = runTest { + whenever(emailDataStoreMock.getEmailUsername()).thenReturn(null) + whenever(emailDataStoreMock.getEmailToken()).thenReturn(null) val value = testee.getValue() @@ -44,47 +45,47 @@ class EmailSyncTest { } @Test - fun whenSaveValueThenStoreCredentials() { + fun whenSaveValueThenStoreCredentials() = runTest { testee.save("{\"username\":\"email\",\"personal_access_token\":\"token\"}") - verify(emailDataStoreMock).emailUsername = "email" - verify(emailDataStoreMock).emailToken = "token" + verify(emailDataStoreMock).setEmailUsername("email") + verify(emailDataStoreMock).setEmailToken("token") } @Test - fun whenSaveNullThenLogoutUser() { + fun whenSaveNullThenLogoutUser() = runTest { testee.save(null) - verify(emailDataStoreMock).emailUsername = "" - verify(emailDataStoreMock).emailToken = "" + verify(emailDataStoreMock).setEmailUsername("") + verify(emailDataStoreMock).setEmailToken("") } @Test - fun whenDeduplicateRemoteAddressWithSameLocalAddressThenDoNothing() { - whenever(emailDataStoreMock.emailUsername).thenReturn("username") - whenever(emailDataStoreMock.emailToken).thenReturn("token") + fun whenDeduplicateRemoteAddressWithSameLocalAddressThenDoNothing() = runTest { + whenever(emailDataStoreMock.getEmailUsername()).thenReturn("username") + whenever(emailDataStoreMock.getEmailToken()).thenReturn("token") testee.deduplicate("{\"username\":\"email\",\"personal_access_token\":\"token\"}") - verify(emailDataStoreMock).emailUsername = "email" - verify(emailDataStoreMock).emailToken = "token" + verify(emailDataStoreMock).setEmailUsername("email") + verify(emailDataStoreMock).setEmailToken("token") } @Test - fun whenDeduplicateRemoteAddressWithDifferentLocalAddressThenRemoteWins() { - whenever(emailDataStoreMock.emailUsername).thenReturn("username2") - whenever(emailDataStoreMock.emailToken).thenReturn("token2") + fun whenDeduplicateRemoteAddressWithDifferentLocalAddressThenRemoteWins() = runTest { + whenever(emailDataStoreMock.getEmailUsername()).thenReturn("username2") + whenever(emailDataStoreMock.getEmailToken()).thenReturn("token2") testee.deduplicate("{\"username\":\"email\",\"personal_access_token\":\"token\"}") - verify(emailDataStoreMock).emailUsername = "email" - verify(emailDataStoreMock).emailToken = "token" + verify(emailDataStoreMock).setEmailUsername("email") + verify(emailDataStoreMock).setEmailToken("token") } @Test - fun whenDeduplicateRemoteAddressWithDifferentLocalAddressThenPixelEvent() { - whenever(emailDataStoreMock.emailUsername).thenReturn("username2") - whenever(emailDataStoreMock.emailToken).thenReturn("token2") + fun whenDeduplicateRemoteAddressWithDifferentLocalAddressThenPixelEvent() = runTest { + whenever(emailDataStoreMock.getEmailUsername()).thenReturn("username2") + whenever(emailDataStoreMock.getEmailToken()).thenReturn("token2") testee.deduplicate("{\"username\":\"email\",\"personal_access_token\":\"token\"}") @@ -92,25 +93,25 @@ class EmailSyncTest { } @Test - fun whenDeduplicateRemoteAddressWithNoLocalAccountThenStoreRemote() { - whenever(emailDataStoreMock.emailUsername).thenReturn(null) - whenever(emailDataStoreMock.emailToken).thenReturn(null) + fun whenDeduplicateRemoteAddressWithNoLocalAccountThenStoreRemote() = runTest { + whenever(emailDataStoreMock.getEmailUsername()).thenReturn(null) + whenever(emailDataStoreMock.getEmailToken()).thenReturn(null) testee.deduplicate("{\"username\":\"email\",\"personal_access_token\":\"token\"}") - verify(emailDataStoreMock).emailUsername = "email" - verify(emailDataStoreMock).emailToken = "token" + verify(emailDataStoreMock).setEmailUsername("email") + verify(emailDataStoreMock).setEmailToken("token") } @Test - fun whenDeduplicateNullAddresThenDoNothing() { - whenever(emailDataStoreMock.emailUsername).thenReturn("username") - whenever(emailDataStoreMock.emailToken).thenReturn("token") + fun whenDeduplicateNullAddresThenDoNothing() = runTest { + whenever(emailDataStoreMock.getEmailUsername()).thenReturn("username") + whenever(emailDataStoreMock.getEmailToken()).thenReturn("token") testee.deduplicate(null) - verify(emailDataStoreMock, times(0)).emailToken - verify(emailDataStoreMock, times(0)).emailUsername + verify(emailDataStoreMock, times(0)).getEmailToken() + verify(emailDataStoreMock, times(0)).getEmailUsername() } companion object { diff --git a/app/src/test/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt index f51860ab43ac..9838c4e0448f 100644 --- a/app/src/test/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt @@ -31,6 +31,7 @@ import com.duckduckgo.subscriptions.api.PrivacyProUnifiedFeedback import com.duckduckgo.subscriptions.api.Subscriptions import com.duckduckgo.sync.api.DeviceSyncState import com.duckduckgo.voice.api.VoiceSearchAvailability +import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Before import org.junit.Rule @@ -131,7 +132,8 @@ class SettingsViewModelTest { } @Test - fun `when Email pressed then pixel is fired`() { + fun `when Email pressed then pixel is fired`() = runTest { + whenever(emailManagerMock.isEmailFeatureSupported()).thenReturn(true) testee.onEmailProtectionSettingClicked() verify(settingsPixelDispatcherMock).fireEmailPressed() diff --git a/app/src/test/java/com/duckduckgo/app/survey/api/SurveyDownloaderTest.kt b/app/src/test/java/com/duckduckgo/app/survey/api/SurveyDownloaderTest.kt index f244954e5c08..19aac028db5d 100644 --- a/app/src/test/java/com/duckduckgo/app/survey/api/SurveyDownloaderTest.kt +++ b/app/src/test/java/com/duckduckgo/app/survey/api/SurveyDownloaderTest.kt @@ -109,7 +109,7 @@ class SurveyDownloaderTest { } @Test - fun whenSurveyForEmailReceivedAndUserIsSignedInThenCreateSurveyWithCorrectCohort() { + fun whenSurveyForEmailReceivedAndUserIsSignedInThenCreateSurveyWithCorrectCohort() = runTest { val surveyWithCohort = Survey("abc", SURVEY_URL_WITH_COHORT, -1, SCHEDULED) whenever(mockSurveyRepository.isUserEligibleForSurvey(surveyWithCohort)).thenReturn(true) whenever(mockEmailManager.isSignedIn()).thenReturn(true) @@ -121,7 +121,7 @@ class SurveyDownloaderTest { } @Test - fun whenSurveyForEmailReceivedAndUserIsNotSignedInThenDoNotCreateSurvey() { + fun whenSurveyForEmailReceivedAndUserIsNotSignedInThenDoNotCreateSurvey() = runTest { whenever(mockEmailManager.isSignedIn()).thenReturn(false) whenever(mockEmailManager.getCohort()).thenReturn("cohort") whenever(mockCall.execute()).thenReturn(Response.success(surveyWithAllocationForEmail("abc"))) diff --git a/app/src/test/java/com/duckduckgo/app/sync/SavedSitesSyncDataProviderTest.kt b/app/src/test/java/com/duckduckgo/app/sync/SavedSitesSyncDataProviderTest.kt index 277761bddc27..148010fcf1c0 100644 --- a/app/src/test/java/com/duckduckgo/app/sync/SavedSitesSyncDataProviderTest.kt +++ b/app/src/test/java/com/duckduckgo/app/sync/SavedSitesSyncDataProviderTest.kt @@ -58,6 +58,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import java.time.OffsetDateTime import java.time.ZoneOffset +import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -201,7 +202,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenFirstSyncAndUsersHasFavoritesThenFormFactorFolderPresent() { + fun whenFirstSyncAndUsersHasFavoritesThenFormFactorFolderPresent() = runTest { repository.insert(favourite1) repository.insert(bookmark3) repository.insert(bookmark4) @@ -221,7 +222,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenNewBookmarksSinceLastSyncThenChangesContainData() { + fun whenNewBookmarksSinceLastSyncThenChangesContainData() = runTest { repository.insert(bookmark3) repository.insert(bookmark4) @@ -235,7 +236,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenOnFirstSyncBookmarkIsInvalidThenChangesDoesNotContainInvalidEntity() { + fun whenOnFirstSyncBookmarkIsInvalidThenChangesDoesNotContainInvalidEntity() = runTest { repository.insert(invalidBookmark) val syncChanges = parser.getChanges() @@ -247,7 +248,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenOnFirstSyncFolderIsInvalidThenChangesContainDataFixed() { + fun whenOnFirstSyncFolderIsInvalidThenChangesContainDataFixed() = runTest { repository.insert(invalidBookmark) repository.insert(invalidFolder) @@ -262,7 +263,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenNewBookmarkIsInvalidThenChangesDoesNotContainInvalidEntity() { + fun whenNewBookmarkIsInvalidThenChangesDoesNotContainInvalidEntity() = runTest { setLastSyncTime(DatabaseDateFormatter.iso8601(twoHoursAgo)) repository.insert(invalidBookmark.copy(lastModified = DatabaseDateFormatter.iso8601(oneHourAgo))) @@ -275,7 +276,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenNewFolderIsInvalidThenChangesContainDataFixed() { + fun whenNewFolderIsInvalidThenChangesContainDataFixed() = runTest { setLastSyncTime(DatabaseDateFormatter.iso8601(twoHoursAgo)) repository.insert(invalidBookmark.copy(lastModified = DatabaseDateFormatter.iso8601(oneHourAgo))) repository.insert(invalidFolder.copy(lastModified = DatabaseDateFormatter.iso8601(oneHourAgo))) @@ -289,7 +290,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenInvalidBookmarkPresentAndValidBookmarkAddedThenOnlyValidBookmarkIncluded() { + fun whenInvalidBookmarkPresentAndValidBookmarkAddedThenOnlyValidBookmarkIncluded() = runTest { repository.insert(invalidBookmark) syncRepository.markSavedSitesAsInvalid(listOf(invalidBookmark.id)) setLastSyncTime(DatabaseDateFormatter.iso8601(twoHoursAgo)) @@ -306,7 +307,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenNewFoldersAndBookmarksAndFavouritesSinceLastSyncThenChangesContainData() { + fun whenNewFoldersAndBookmarksAndFavouritesSinceLastSyncThenChangesContainData() = runTest { val modificationTimestamp = DatabaseDateFormatter.iso8601() val lastSyncTimestamp = DatabaseDateFormatter.iso8601(twoHoursAgo) setLastSyncTime(lastSyncTimestamp) @@ -365,7 +366,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenNoChangesAfterLastSyncThenChangesAreEmpty() { + fun whenNoChangesAfterLastSyncThenChangesAreEmpty() = runTest { val modificationTimestamp = DatabaseDateFormatter.iso8601(twoHoursAgo) val lastSyncTimestamp = DatabaseDateFormatter.iso8601() setLastSyncTime(lastSyncTimestamp) @@ -496,7 +497,7 @@ class SavedSitesSyncDataProviderTest { } @Test - fun whenChangesNeedSyncThenChildrenRequestIsAddedToMetadata() { + fun whenChangesNeedSyncThenChildrenRequestIsAddedToMetadata() = runTest { repository.insert(bookmark3) repository.insert(bookmark4) diff --git a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt index 3da1d4edab25..3b12ebbbdb8e 100644 --- a/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModelTest.kt @@ -1537,7 +1537,7 @@ class TabSwitcherViewModelTest { TODO("Not yet implemented") } - override fun emailEnabled(): Boolean { + override suspend fun emailEnabled(): Boolean { TODO("Not yet implemented") } diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/email/EmailManager.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/email/EmailManager.kt index 208426b59df3..d93908db7814 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/email/EmailManager.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/email/EmailManager.kt @@ -31,22 +31,22 @@ interface EmailManager { /** * Indicates if the user is signed in or not */ - fun isSignedIn(): Boolean + suspend fun isSignedIn(): Boolean /** * Get the next available private duck address alias */ - fun getAlias(): String? + suspend fun getAlias(): String? /** * Get the stored auth token */ - fun getToken(): String? + suspend fun getToken(): String? /** * Store the credentials for the user's duck address */ - fun storeCredentials( + suspend fun storeCredentials( token: String, username: String, cohort: String, @@ -55,35 +55,35 @@ interface EmailManager { /** * Signs out of using duck addresses on this device */ - fun signOut() + suspend fun signOut() /** * Get the user's full, personal duck address */ - fun getEmailAddress(): String? + suspend fun getEmailAddress(): String? /** * Get the user's duck address data in a format that can be passed to JS */ - fun getUserData(): String + suspend fun getUserData(): String /** * Get the cohort */ - fun getCohort(): String + suspend fun getCohort(): String /** * Determines if duck address can be used on this device */ - fun isEmailFeatureSupported(): Boolean + suspend fun isEmailFeatureSupported(): Boolean /** * Return last used date */ - fun getLastUsedDate(): String + suspend fun getLastUsedDate(): String /** * Updates the last used date */ - fun setNewLastUsedDate() + suspend fun setNewLastUsedDate() } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt index 2329277aac08..d163194a3e28 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/configuration/AutofillRuntimeConfigProvider.kt @@ -152,7 +152,7 @@ class RealAutofillRuntimeConfigProvider @Inject constructor( return emailProtectionInContextAvailabilityRules.permittedToShow(url) } - private fun determineIfEmailAvailable(): Boolean = emailManager.isSignedIn() + private suspend fun determineIfEmailAvailable(): Boolean = emailManager.isSignedIn() companion object { private const val TAG_INJECT_CONTENT_SCOPE = "// INJECT contentScope HERE" diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt index f6f71db6a02b..195820ced2db 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmail.kt @@ -80,7 +80,11 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor( onSelectedToUsePrivateAlias(originalUrl, autofillCallback) notifyAutofillListenersDuckAddressFilled() } - DoNotUseEmailProtection -> onSelectedNotToUseEmailProtection() + DoNotUseEmailProtection -> { + appCoroutineScope.launch(dispatchers.io()) { + onSelectedNotToUseEmailProtection() + } + } } } @@ -114,11 +118,11 @@ class ResultHandlerEmailProtectionChooseEmail @Inject constructor( } } - private fun onSelectedNotToUseEmailProtection() { + private suspend fun onSelectedNotToUseEmailProtection() { enqueueEmailProtectionPixel(EMAIL_TOOLTIP_DISMISSED, includeLastUsedDay = false) } - private fun enqueueEmailProtectionPixel( + private suspend fun enqueueEmailProtectionPixel( pixelName: PixelName, includeLastUsedDay: Boolean, ) { diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/AutofillBreakageReportSender.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/AutofillBreakageReportSender.kt index 1d32ff8b7b1d..c0377c9dd229 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/AutofillBreakageReportSender.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/reporting/AutofillBreakageReportSender.kt @@ -85,7 +85,7 @@ class AutofillBreakageReportSenderImpl @Inject constructor( return neverSavedSiteRepository.isInNeverSaveList(url).toString() } - private fun formatEmailProtectionStatus(): String { + private suspend fun formatEmailProtectionStatus(): String { return emailManager.isSignedIn().toString() } diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/sync/provider/CredentialsSyncDataProvider.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/sync/provider/CredentialsSyncDataProvider.kt index 3be8ff483900..83ac53de4fec 100644 --- a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/sync/provider/CredentialsSyncDataProvider.kt +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/sync/provider/CredentialsSyncDataProvider.kt @@ -31,7 +31,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import javax.inject.* -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext @ContributesMultibinding(scope = AppScope::class, boundType = SyncableDataProvider::class) class CredentialsSyncDataProvider @Inject constructor( @@ -42,16 +42,16 @@ class CredentialsSyncDataProvider @Inject constructor( ) : SyncableDataProvider { override fun getType(): SyncableType = CREDENTIALS - override fun getChanges(): SyncChangesRequest { + override suspend fun getChanges(): SyncChangesRequest { if (appBuildConfig.isInternalBuild()) checkMainThread() - return runBlocking(dispatchers.io()) { + return withContext(dispatchers.io()) { if (credentialsSyncStore.serverModifiedSince == "0") { credentialsSync.initMetadata() } val since = credentialsSyncStore.clientModifiedSince val updates = credentialsSync.getUpdatesSince(since) val request = formatUpdates(updates) - return@runBlocking request + return@withContext request } } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillRuntimeConfigProviderTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillRuntimeConfigProviderTest.kt index 39a33aac0d49..272a36f6d3cc 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillRuntimeConfigProviderTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/configuration/RealAutofillRuntimeConfigProviderTest.kt @@ -100,30 +100,40 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenAutofillNotEnabledThenConfigurationUserPrefsCredentialsIsFalse() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) configureAutofillCapabilities(enabled = false) + testee.getRuntimeConfiguration("", EXAMPLE_URL) + verifyAutofillCredentialsReturnedAs(false) } @Test fun whenAutofillEnabledThenConfigurationUserPrefsCredentialsIsTrue() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) configureAutofillCapabilities(enabled = true) configureNoShareableLogins() + testee.getRuntimeConfiguration("", EXAMPLE_URL) + verifyAutofillCredentialsReturnedAs(true) } @Test fun whenCanAutofillThenConfigSpecifiesShowingKeyIcon() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) configureAutofillCapabilities(enabled = true) configureAutofillAvailableForSite(EXAMPLE_URL) configureNoShareableLogins() + testee.getRuntimeConfiguration("", EXAMPLE_URL) + verifyKeyIconRequestedToShow() } @Test fun whenNoCredentialsForUrlThenConfigurationInputTypeCredentialsIsFalse() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) configureAutofillEnabledWithNoSavedCredentials(EXAMPLE_URL) testee.getRuntimeConfiguration("", EXAMPLE_URL) @@ -136,6 +146,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenWithCredentialsForUrlThenConfigurationInputTypeCredentialsIsTrue() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) configureAutofillCapabilities(enabled = true) whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn( listOf( @@ -160,6 +171,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenWithShareableCredentialsForUrlThenConfigurationInputTypeCredentialsIsTrue() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) configureAutofillCapabilities(enabled = true) whenever(autofillStore.getCredentials(EXAMPLE_URL)).thenReturn(emptyList()) whenever(shareableCredentials.shareableCredentials(any())).thenReturn( @@ -184,6 +196,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenWithUsernameOnlyForUrlThenConfigurationInputTypeCredentialsUsernameIsTrue() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) val url = "example.com" configureAutofillCapabilities(enabled = true) whenever(autofillStore.getCredentials(url)).thenReturn( @@ -209,6 +222,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenWithEmptyUsernameOnlyForUrlThenConfigurationInputTypeCredentialsUsernameIsFalse() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) val url = "example.com" configureAutofillCapabilities(enabled = true) whenever(autofillStore.getCredentials(url)).thenReturn( @@ -234,6 +248,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenWithPasswordOnlyForUrlThenConfigurationInputTypeCredentialsUsernameIsTrue() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) val url = "example.com" configureAutofillCapabilities(enabled = true) whenever(autofillStore.getCredentials(url)).thenReturn( @@ -259,6 +274,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenWithEmptyPasswordOnlyForUrlThenConfigurationInputTypeCredentialsUsernameIsTrue() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) val url = "example.com" configureAutofillCapabilities(enabled = true) whenever(autofillStore.getCredentials(url)).thenReturn( @@ -284,6 +300,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenWithCredentialsForUrlButAutofillDisabledThenConfigurationInputTypeCredentialsIsFalse() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) val url = "example.com" configureAutofillCapabilities(enabled = false) whenever(autofillStore.getCredentials(url)).thenReturn( @@ -308,6 +325,7 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenWithCredentialsForUrlButAutofillUnavailableThenConfigurationInputTypeCredentialsIsFalse() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) val url = "example.com" configureAutofillCapabilities(enabled = false) whenever(autofillStore.getCredentials(url)).thenReturn( @@ -360,19 +378,24 @@ class RealAutofillRuntimeConfigProviderTest { @Test fun whenSiteNotInNeverSaveListThenCanSaveCredentials() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) val url = "example.com" configureAutofillEnabledWithNoSavedCredentials(url) + testee.getRuntimeConfiguration("", url) + verifyCanSaveCredentialsReturnedAs(true) } @Test fun whenSiteInNeverSaveListThenStillTellJsWeCanSaveCredentials() = runTest { + whenever(emailManager.isSignedIn()).thenReturn(false) val url = "example.com" configureAutofillEnabledWithNoSavedCredentials(url) whenever(neverSavedSiteRepository.isInNeverSaveList(url)).thenReturn(true) testee.getRuntimeConfiguration("", url) + verifyCanSaveCredentialsReturnedAs(true) } diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmailTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmailTest.kt index 9f6ce7f64139..788bbceb4dc0 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmailTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/email/ResultHandlerEmailProtectionChooseEmailTest.kt @@ -69,7 +69,7 @@ class ResultHandlerEmailProtectionChooseEmailTest { ) @Before - fun before() { + fun before() = runTest { whenever(emailManager.getEmailAddress()).thenReturn("personal-example@duck.com") whenever(emailManager.getAlias()).thenReturn("private-example@duck.com") whenever(emailManager.getCohort()).thenReturn("cohort") diff --git a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/repository/RemoteDuckAddressStatusRepositoryTest.kt b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/repository/RemoteDuckAddressStatusRepositoryTest.kt index 3d48f1e862b5..6a4b1d8c4c60 100644 --- a/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/repository/RemoteDuckAddressStatusRepositoryTest.kt +++ b/autofill/autofill-impl/src/test/java/com/duckduckgo/autofill/impl/ui/credential/repository/RemoteDuckAddressStatusRepositoryTest.kt @@ -153,6 +153,6 @@ class RemoteDuckAddressStatusRepositoryTest { whenever(service.getActivationStatus(any(), any())).thenAnswer { throw IOException() } } - private fun configureEmailProtectionNotSignedIn() = whenever(emailManager.getToken()).thenReturn(null) - private fun configureEmailProtectionSignedIn() = whenever(emailManager.getToken()).thenReturn("abc123") + private suspend fun configureEmailProtectionNotSignedIn() = whenever(emailManager.getToken()).thenReturn(null) + private suspend fun configureEmailProtectionSignedIn() = whenever(emailManager.getToken()).thenReturn("abc123") } diff --git a/browser-api/src/main/java/com/duckduckgo/browser/api/UserBrowserProperties.kt b/browser-api/src/main/java/com/duckduckgo/browser/api/UserBrowserProperties.kt index 0874ff229745..c97b4c55a0f8 100644 --- a/browser-api/src/main/java/com/duckduckgo/browser/api/UserBrowserProperties.kt +++ b/browser-api/src/main/java/com/duckduckgo/browser/api/UserBrowserProperties.kt @@ -26,7 +26,7 @@ interface UserBrowserProperties { fun daysSinceInstalled(): Long suspend fun daysUsedSince(since: Date): Long fun defaultBrowser(): Boolean - fun emailEnabled(): Boolean + suspend fun emailEnabled(): Boolean fun searchCount(): Long fun widgetAdded(): Boolean } diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/DisplayModeSyncableSetting.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/DisplayModeSyncableSetting.kt index 59d2e205dc51..ff1de5fa0238 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/DisplayModeSyncableSetting.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/DisplayModeSyncableSetting.kt @@ -37,11 +37,11 @@ class DisplayModeSyncableSetting @Inject constructor( override val key: String = "favorites_display_mode" - override fun getValue(): String? { + override suspend fun getValue(): String? { return savedSitesSettingsStore.favoritesDisplayMode.value } - override fun save(value: String?): Boolean { + override suspend fun save(value: String?): Boolean { Timber.i("Sync-Settings-Display-Mode: save, value received $value") val displayMode = FavoritesDisplayMode.values().firstOrNull { it.value == value } ?: return false Timber.i("Sync-Settings-Display-Mode: save, storing($displayMode)") @@ -50,7 +50,7 @@ class DisplayModeSyncableSetting @Inject constructor( return true } - override fun deduplicate(value: String?): Boolean { + override suspend fun deduplicate(value: String?): Boolean { Timber.i("Sync-Settings-Display-Mode: deduplicate, value received $value") val displayMode = FavoritesDisplayMode.values().firstOrNull { it.value == value } ?: return false Timber.i("Sync-Settings-Display-Mode: deduplicate, storing ($displayMode)") diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncDataProvider.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncDataProvider.kt index fe895e283dde..b8db411479d3 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncDataProvider.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/sync/SavedSitesSyncDataProvider.kt @@ -44,7 +44,7 @@ class SavedSitesSyncDataProvider @Inject constructor( ) : SyncableDataProvider { override fun getType(): SyncableType = BOOKMARKS - override fun getChanges(): SyncChangesRequest { + override suspend fun getChanges(): SyncChangesRequest { savedSitesSyncStore.startTimeStamp = DatabaseDateFormatter.iso8601() val updates = if (savedSitesSyncStore.serverModifiedSince == "0") { savedSitesFormFactorSyncMigration.onFormFactorFavouritesEnabled() diff --git a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/api/BrowserFeatureStateReporterPlugin.kt b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/api/BrowserFeatureStateReporterPlugin.kt index 928cdeacaf8d..96ccf4e24684 100644 --- a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/api/BrowserFeatureStateReporterPlugin.kt +++ b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/api/BrowserFeatureStateReporterPlugin.kt @@ -22,5 +22,5 @@ interface BrowserFeatureStateReporterPlugin { * Used to report the state across different modules in the browser * @return a map of key-value pairs that represent the state of the features */ - fun featureStateParams(): Map + suspend fun featureStateParams(): Map } diff --git a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt index b3eaa93c34d9..2b14aa39f2d4 100644 --- a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt +++ b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/AtbInitializer.kt @@ -55,7 +55,7 @@ class AtbInitializer @Inject constructor( appCoroutineScope.launch(dispatcherProvider.io()) { refreshAppRetentionAtb() } } - private fun refreshAppRetentionAtb() { + private suspend fun refreshAppRetentionAtb() { if (statisticsDataStore.hasInstallationStatistics) { statisticsUpdater.refreshAppRetentionAtb() } diff --git a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/api/StatisticsRequester.kt b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/api/StatisticsRequester.kt index dfd280a8b4c7..6fca94e2363c 100644 --- a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/api/StatisticsRequester.kt +++ b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/api/StatisticsRequester.kt @@ -29,13 +29,13 @@ import com.squareup.anvil.annotations.ContributesBinding import io.reactivex.schedulers.Schedulers import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber interface StatisticsUpdater { - fun initializeAtb() - fun refreshSearchRetentionAtb() - fun refreshAppRetentionAtb() + suspend fun initializeAtb() + suspend fun refreshSearchRetentionAtb() + suspend fun refreshAppRetentionAtb() } @ContributesBinding(AppScope::class) @@ -54,7 +54,7 @@ class StatisticsRequester @Inject constructor( * consume referer data */ @SuppressLint("CheckResult") - override fun initializeAtb() { + override suspend fun initializeAtb() { Timber.i("Initializing ATB") if (store.hasInstallationStatistics) { @@ -101,15 +101,15 @@ class StatisticsRequester @Inject constructor( storedAtb.version.endsWith(LEGACY_ATB_FORMAT_SUFFIX) @SuppressLint("CheckResult") - override fun refreshSearchRetentionAtb() { - val atb = store.atb + override suspend fun refreshSearchRetentionAtb() { + withContext(dispatchers.io()) { + val atb = store.atb - if (atb == null) { - initializeAtb() - return - } + if (atb == null) { + initializeAtb() + return@withContext + } - appCoroutineScope.launch(dispatchers.io()) { val fullAtb = atb.formatWithVariant(variantManager.getVariantKey()) val oldSearchAtb = store.searchRetentionAtb ?: atb.version @@ -133,7 +133,7 @@ class StatisticsRequester @Inject constructor( } @SuppressLint("CheckResult") - override fun refreshAppRetentionAtb() { + override suspend fun refreshAppRetentionAtb() { val atb = store.atb if (atb == null) { @@ -162,7 +162,7 @@ class StatisticsRequester @Inject constructor( ) } - private fun emailSignInState(): Int = + private suspend fun emailSignInState(): Int = kotlin.runCatching { emailManager.isSignedIn().asInt() }.getOrDefault(0) private fun storeUpdateVersionIfPresent(retrievedAtb: Atb) { diff --git a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/pixels/FeatureRetentionPixelSender.kt b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/pixels/FeatureRetentionPixelSender.kt index 8f5d9b8ebd72..98764377eec6 100644 --- a/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/pixels/FeatureRetentionPixelSender.kt +++ b/statistics/statistics-impl/src/main/java/com/duckduckgo/app/statistics/pixels/FeatureRetentionPixelSender.kt @@ -60,7 +60,7 @@ class FeatureRetentionPixelSender @Inject constructor( } } - private fun tryToFireDailyPixel( + private suspend fun tryToFireDailyPixel( pixelName: String, ) { val now = getUtcIsoLocalDate() diff --git a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/api/StatisticsRequesterTest.kt b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/api/StatisticsRequesterTest.kt index ec9a5242b25b..d268a04036a5 100644 --- a/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/api/StatisticsRequesterTest.kt +++ b/statistics/statistics-impl/src/test/java/com/duckduckgo/app/statistics/api/StatisticsRequesterTest.kt @@ -25,6 +25,7 @@ import com.duckduckgo.common.utils.plugins.PluginPoint import com.duckduckgo.experiments.api.VariantManager import io.reactivex.Observable import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import okhttp3.ResponseBody import org.junit.Before import org.junit.Rule @@ -76,7 +77,7 @@ class StatisticsRequesterTest { } @Test - fun whenUpdateVersionPresentDuringRefreshSearchRetentionThenPreviousAtbIsReplacedWithUpdateVersion() { + fun whenUpdateVersionPresentDuringRefreshSearchRetentionThenPreviousAtbIsReplacedWithUpdateVersion() = runTest { configureStoredStatistics() whenever(mockService.updateSearchAtb(any(), any(), any(), eq(0))).thenReturn(Observable.just(UPDATE_ATB)) testee.refreshSearchRetentionAtb() @@ -84,7 +85,7 @@ class StatisticsRequesterTest { } @Test - fun whenUpdateVersionPresentDuringRefreshSearchRetentionThenPreviousVariantIsReplacedWithDefaultVariant() { + fun whenUpdateVersionPresentDuringRefreshSearchRetentionThenPreviousVariantIsReplacedWithDefaultVariant() = runTest { configureStoredStatistics() whenever(mockService.updateSearchAtb(any(), any(), any(), eq(0))).thenReturn(Observable.just(UPDATE_ATB)) testee.refreshSearchRetentionAtb() @@ -92,7 +93,7 @@ class StatisticsRequesterTest { } @Test - fun whenUpdateVersionPresentDuringRefreshAppRetentionThenPreviousAtbIsReplacedWithUpdateVersion() { + fun whenUpdateVersionPresentDuringRefreshAppRetentionThenPreviousAtbIsReplacedWithUpdateVersion() = runTest { configureStoredStatistics() whenever(mockService.updateAppAtb(any(), any(), any(), eq(0))).thenReturn(Observable.just(UPDATE_ATB)) testee.refreshAppRetentionAtb() @@ -100,7 +101,7 @@ class StatisticsRequesterTest { } @Test - fun whenUpdateVersionPresentDuringRefreshAppRetentionThenPreviousVariantIsReplacedWithDefaultVariant() { + fun whenUpdateVersionPresentDuringRefreshAppRetentionThenPreviousVariantIsReplacedWithDefaultVariant() = runTest { configureStoredStatistics() whenever(mockService.updateAppAtb(any(), any(), any(), eq(0))).thenReturn(Observable.just(UPDATE_ATB)) testee.refreshAppRetentionAtb() @@ -108,7 +109,7 @@ class StatisticsRequesterTest { } @Test - fun whenNoStatisticsStoredThenInitializeAtbInvokesExti() { + fun whenNoStatisticsStoredThenInitializeAtbInvokesExti() = runTest { configureNoStoredStatistics() testee.initializeAtb() verify(mockService).atb(any(), eq(0)) @@ -117,7 +118,7 @@ class StatisticsRequesterTest { } @Test - fun whenStatisticsStoredThenInitializeAtbDoesNothing() { + fun whenStatisticsStoredThenInitializeAtbDoesNothing() = runTest { configureStoredStatistics() testee.initializeAtb() verify(mockService, never()).atb(any(), any()) @@ -125,7 +126,7 @@ class StatisticsRequesterTest { } @Test - fun whenNoStatisticsStoredThenRefreshSearchRetentionRetrievesAtbAndInvokesExti() { + fun whenNoStatisticsStoredThenRefreshSearchRetentionRetrievesAtbAndInvokesExti() = runTest { configureNoStoredStatistics() testee.refreshSearchRetentionAtb() verify(mockService).atb(any(), any()) @@ -134,7 +135,7 @@ class StatisticsRequesterTest { } @Test - fun whenNoStatisticsStoredThenRefreshAppRetentionRetrievesAtbAndInvokesExti() { + fun whenNoStatisticsStoredThenRefreshAppRetentionRetrievesAtbAndInvokesExti() = runTest { configureNoStoredStatistics() testee.refreshAppRetentionAtb() verify(mockService).atb(any(), any()) @@ -143,7 +144,7 @@ class StatisticsRequesterTest { } @Test - fun whenExtiFailsThenAtbCleared() { + fun whenExtiFailsThenAtbCleared() = runTest { whenever(mockService.exti(any(), any())).thenReturn(Observable.error(Throwable())) configureNoStoredStatistics() testee.initializeAtb() @@ -152,7 +153,7 @@ class StatisticsRequesterTest { } @Test - fun whenStatisticsStoredThenRefreshIncludesRefreshedAtb() { + fun whenStatisticsStoredThenRefreshIncludesRefreshedAtb() = runTest { configureStoredStatistics() val retentionAtb = "foo" whenever(mockStatisticsStore.searchRetentionAtb).thenReturn(retentionAtb) @@ -161,7 +162,7 @@ class StatisticsRequesterTest { } @Test - fun whenStatisticsStoredThenRefreshUpdatesAtb() { + fun whenStatisticsStoredThenRefreshUpdatesAtb() = runTest { configureStoredStatistics() testee.refreshSearchRetentionAtb() verify(mockService).updateSearchAtb(eq(ATB_WITH_VARIANT), eq(ATB.version), any(), any()) @@ -169,7 +170,7 @@ class StatisticsRequesterTest { } @Test - fun whenAlreadyInitializedWithLegacyAtbThenInitializationRemovesLegacyVariant() { + fun whenAlreadyInitializedWithLegacyAtbThenInitializationRemovesLegacyVariant() = runTest { configureStoredStatistics() whenever(mockVariantManager.defaultVariantKey()).thenReturn("") whenever(mockStatisticsStore.atb).thenReturn(Atb("v123ma")) @@ -179,7 +180,7 @@ class StatisticsRequesterTest { } @Test - fun whenInitializeAtbAndEmailEnabledThenEmailSignalToTrue() { + fun whenInitializeAtbAndEmailEnabledThenEmailSignalToTrue() = runTest { whenever(mockEmailManager.isSignedIn()).thenReturn(true) configureNoStoredStatistics() @@ -189,7 +190,7 @@ class StatisticsRequesterTest { } @Test - fun whenRefreshSearchAtbAndEmailEnabledThenEmailSignalToTrue() { + fun whenRefreshSearchAtbAndEmailEnabledThenEmailSignalToTrue() = runTest { whenever(mockEmailManager.isSignedIn()).thenReturn(true) configureStoredStatistics() whenever(mockService.updateSearchAtb(any(), any(), any(), eq(1))).thenReturn(Observable.just(UPDATE_ATB)) @@ -200,7 +201,7 @@ class StatisticsRequesterTest { } @Test - fun whenRefreshAppRetentionAndEmailEnabledThenEmailSignalToTrue() { + fun whenRefreshAppRetentionAndEmailEnabledThenEmailSignalToTrue() = runTest { whenever(mockEmailManager.isSignedIn()).thenReturn(true) configureStoredStatistics() whenever(mockService.updateAppAtb(any(), any(), any(), eq(1))).thenReturn(Observable.just(UPDATE_ATB)) diff --git a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncEngine.kt b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncEngine.kt index e984eb4f388d..1777485372d9 100644 --- a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncEngine.kt +++ b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncEngine.kt @@ -22,7 +22,7 @@ interface SyncEngine { * Entry point to the Sync Engine * See Tech Design: Sync Updating/Polling Strategy https://app.asana.com/0/481882893211075/1204040479708519/f */ - fun triggerSync(trigger: SyncTrigger) + suspend fun triggerSync(trigger: SyncTrigger) /** * Sync Feature has been disabled / device has been removed diff --git a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncableDataProvider.kt b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncableDataProvider.kt index f30362edbc90..c2fe3f864049 100644 --- a/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncableDataProvider.kt +++ b/sync/sync-api/src/main/java/com/duckduckgo/sync/api/engine/SyncableDataProvider.kt @@ -24,5 +24,5 @@ interface SyncableDataProvider { * since a specific time * This data that will be sent to the Sync API */ - fun getChanges(): SyncChangesRequest + suspend fun getChanges(): SyncChangesRequest } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt index 0f5df5b3a9d9..989d340f5923 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncAccountRepository.kt @@ -52,9 +52,9 @@ interface SyncAccountRepository { fun getCodeType(stringCode: String): CodeType fun isSyncSupported(): Boolean - fun createAccount(): Result + suspend fun createAccount(): Result fun isSignedIn(): Boolean - fun processCode(stringCode: String): Result + suspend fun processCode(stringCode: String): Result fun getAccountInfo(): AccountInfo fun logout(deviceId: String): Result fun deleteAccount(): Result @@ -68,7 +68,7 @@ interface SyncAccountRepository { fun pollSecondDeviceExchangeAcknowledgement(): Result fun pollForRecoveryCodeAndLogin(): Result fun renameDevice(device: ConnectedDevice): Result - fun logoutAndJoinNewAccount(stringCode: String): Result + suspend fun logoutAndJoinNewAccount(stringCode: String): Result } @ContributesBinding(AppScope::class) @@ -103,7 +103,7 @@ class AppSyncAccountRepository @Inject constructor( return syncStore.isEncryptionSupported() } - override fun createAccount(): Result { + override suspend fun createAccount(): Result { if (isSignedIn()) { return Error(code = ALREADY_SIGNED_IN.code, reason = "Already signed in") .alsoFireAlreadySignedInErrorPixel() @@ -114,7 +114,7 @@ class AppSyncAccountRepository @Inject constructor( } } - override fun processCode(stringCode: String): Result { + override suspend fun processCode(stringCode: String): Result { val decodedCode: String? = kotlin.runCatching { return@runCatching stringCode.decodeB64() }.getOrNull() @@ -347,7 +347,7 @@ class AppSyncAccountRepository @Inject constructor( return Success(linkingQRCode.encodeB64()) } - private fun connectDevice(connectKeys: ConnectCode): Result { + private suspend fun connectDevice(connectKeys: ConnectCode): Result { if (!isSignedIn()) { performCreateAccount().onFailure { it.alsoFireSignUpErrorPixel() @@ -587,7 +587,7 @@ class AppSyncAccountRepository @Inject constructor( override fun isSignedIn() = syncStore.isSignedIn() - override fun logoutAndJoinNewAccount(stringCode: String): Result { + override suspend fun logoutAndJoinNewAccount(stringCode: String): Result { val thisDeviceId = syncStore.deviceId.orEmpty() return when (val result = logout(thisDeviceId)) { is Error -> { @@ -637,7 +637,7 @@ class AppSyncAccountRepository @Inject constructor( } } - private fun performCreateAccount(): Result { + private suspend fun performCreateAccount(): Result { val userId = syncDeviceIds.userId() val account: AccountKeys = kotlin.runCatching { nativeLib.generateAccountKeys(userId = userId).also { diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/engine/RealSyncEngine.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/engine/RealSyncEngine.kt index 17b29ca77391..b62fb864b7f1 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/engine/RealSyncEngine.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/engine/RealSyncEngine.kt @@ -67,7 +67,7 @@ class RealSyncEngine @Inject constructor( private val lifecyclePlugins: PluginPoint, ) : SyncEngine { - override fun triggerSync(trigger: SyncTrigger) { + override suspend fun triggerSync(trigger: SyncTrigger) { Timber.i("Sync-Engine: petition to sync now trigger: $trigger") if (syncStore.isSignedIn() && syncStore.syncingDataEnabled) { Timber.d("Sync-Engine: sync enabled, triggering operation: $trigger") @@ -90,7 +90,7 @@ class RealSyncEngine @Inject constructor( } } - private fun scheduleSync(trigger: SyncTrigger) { + private suspend fun scheduleSync(trigger: SyncTrigger) { when (syncScheduler.scheduleOperation()) { DISCARD -> { Timber.d("Sync-Engine: petition to sync debounced") @@ -103,7 +103,7 @@ class RealSyncEngine @Inject constructor( } } - private fun sendLocalData() { + private suspend fun sendLocalData() { Timber.d("Sync-Engine: initiating first sync") syncStateRepository.store(SyncAttempt(state = IN_PROGRESS, meta = "Account Creation")) getChanges().forEach { @@ -119,7 +119,7 @@ class RealSyncEngine @Inject constructor( syncStateRepository.updateSyncState(SUCCESS) } - private fun performSync(trigger: SyncTrigger) { + private suspend fun performSync(trigger: SyncTrigger) { if (syncInProgress()) { Timber.d("Sync-Engine: sync already in progress, throttling") } else { @@ -151,7 +151,7 @@ class RealSyncEngine @Inject constructor( } } - private fun performFirstSync(firstSyncChanges: List) { + private suspend fun performFirstSync(firstSyncChanges: List) { val types = firstSyncChanges.map { it.type } firstSyncChanges.forEach { changes -> @@ -219,7 +219,7 @@ class RealSyncEngine @Inject constructor( } } - private fun getChanges(): List { + private suspend fun getChanges(): List { return providerPlugins.getPlugins().mapNotNull { Timber.d("Sync-Engine: asking for changes in ${it.javaClass}") kotlin.runCatching { diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt index e010aee5e27f..210abee8d2e9 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/AppSyncAccountRepositoryTest.kt @@ -77,6 +77,7 @@ import com.duckduckgo.sync.impl.pixels.SyncPixels import com.duckduckgo.sync.store.SyncStore import com.squareup.moshi.Moshi import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNull @@ -132,7 +133,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenCreateAccountSucceedsThenAccountPersisted() { + fun whenCreateAccountSucceedsThenAccountPersisted() = runTest { prepareToProvideDeviceIds() prepareForCreateAccountSuccess() @@ -150,7 +151,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenUserSignedInCreatesAccountThenReturnAlreadySignedInError() { + fun whenUserSignedInCreatesAccountThenReturnAlreadySignedInError() = runTest { whenever(syncStore.isSignedIn()).thenReturn(true) val result = syncRepo.createAccount() as Error @@ -159,7 +160,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenCreateAccountFailsThenReturnCreateAccountError() { + fun whenCreateAccountFailsThenReturnCreateAccountError() = runTest { prepareToProvideDeviceIds() prepareForEncryption() whenever(nativeLib.generateAccountKeys(userId = anyString(), password = anyString())).thenReturn(accountKeys) @@ -172,7 +173,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenCreateAccountGenerateKeysFailsThenReturnCreateAccountError() { + fun whenCreateAccountGenerateKeysFailsThenReturnCreateAccountError() = runTest { prepareToProvideDeviceIds() whenever(nativeLib.generateAccountKeys(userId = anyString(), password = anyString())).thenReturn(accountKeysFailed) whenever(syncApi.createAccount(anyString(), anyString(), anyString(), anyString(), anyString(), anyString())) @@ -243,7 +244,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenProcessJsonRecoveryCodeSucceedsThenAccountPersisted() { + fun whenProcessJsonRecoveryCodeSucceedsThenAccountPersisted() = runTest { prepareForLoginSuccess() val result = syncRepo.processCode(jsonRecoveryKeyEncoded) @@ -260,7 +261,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenExchangeCodeProcessedButFeatureFlagIsDisabledThenIsError() { + fun whenExchangeCodeProcessedButFeatureFlagIsDisabledThenIsError() = runTest { prepareForExchangeSuccess() syncFeature.exchangeKeysToSyncWithAnotherDevice().setRawStoredState(State(false)) @@ -271,7 +272,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenExchangeCodeProcessedThenInvitationAccepted() { + fun whenExchangeCodeProcessedThenInvitationAccepted() = runTest { prepareForExchangeSuccess() val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) @@ -284,7 +285,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenExchangeCodeProcessedAndInvitationAcceptedRequestFailedThenIsError() { + fun whenExchangeCodeProcessedAndInvitationAcceptedRequestFailedThenIsError() = runTest { syncFeature.exchangeKeysToSyncWithAnotherDevice().setRawStoredState(State(true)) prepareForExchangeSuccess() @@ -331,7 +332,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenPollingForRecoveryCodeReturnsUnexpectedResponseThenIsError() { + fun whenPollingForRecoveryCodeReturnsUnexpectedResponseThenIsError() = runTest { prepareForExchangeSuccess() val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) @@ -345,7 +346,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenPollingForRecoveryCodeSuccessfulAndNotAlreadySignedInThenIsLoggedIn() { + fun whenPollingForRecoveryCodeSuccessfulAndNotAlreadySignedInThenIsLoggedIn() = runTest { prepareForExchangeSuccess() val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) @@ -360,7 +361,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenPollingForRecoveryCodeSuccessfulAndAlreadySignedInSingleDeviceThenIsLoggedIn() { + fun whenPollingForRecoveryCodeSuccessfulAndAlreadySignedInSingleDeviceThenIsLoggedIn() = runTest { prepareForExchangeSuccess() val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) @@ -376,7 +377,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenPollingForRecoveryCodeSuccessfulAndAlreadySignedInMultipleDevicesThenAccountSwitchingRequired() { + fun whenPollingForRecoveryCodeSuccessfulAndAlreadySignedInMultipleDevicesThenAccountSwitchingRequired() = runTest { prepareForExchangeSuccess() val exchangeCode = jsonExchangeKey(primaryDeviceKeyId, validLoginKeys.primaryKey) @@ -440,7 +441,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenSignedInAndProcessRecoveryCodeIfAllowSwitchAccountTrueThenSwitchAccountIfOnly1DeviceConnected() { + fun whenSignedInAndProcessRecoveryCodeIfAllowSwitchAccountTrueThenSwitchAccountIfOnly1DeviceConnected() = runTest { givenAuthenticatedDevice() givenAccountWithConnectedDevices(1) doAnswer { @@ -458,7 +459,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenSignedInAndProcessRecoveryCodeIfAllowSwitchAccountTrueThenReturnErrorIfMultipleDevicesConnected() { + fun whenSignedInAndProcessRecoveryCodeIfAllowSwitchAccountTrueThenReturnErrorIfMultipleDevicesConnected() = runTest { givenAuthenticatedDevice() givenAccountWithConnectedDevices(2) doAnswer { @@ -473,7 +474,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenLogoutAndJoinNewAccountSucceedsThenReturnSuccess() { + fun whenLogoutAndJoinNewAccountSucceedsThenReturnSuccess() = runTest { givenAuthenticatedDevice() doAnswer { givenUnauthenticatedDevice() // simulate logout locally @@ -496,7 +497,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenGenerateKeysFromRecoveryCodeFailsThenReturnLoginFailedError() { + fun whenGenerateKeysFromRecoveryCodeFailsThenReturnLoginFailedError() = runTest { prepareToProvideDeviceIds() whenever(nativeLib.prepareForLogin(primaryKey = primaryKey)).thenReturn(failedLoginKeys) @@ -506,7 +507,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenLoginFailsThenReturnLoginFailedError() { + fun whenLoginFailsThenReturnLoginFailedError() = runTest { prepareToProvideDeviceIds() prepareForEncryption() whenever(nativeLib.prepareForLogin(primaryKey = primaryKey)).thenReturn(validLoginKeys) @@ -518,7 +519,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenProcessRecoveryKeyAndDecryptSecretKeyFailsThenReturnLoginFailedError() { + fun whenProcessRecoveryKeyAndDecryptSecretKeyFailsThenReturnLoginFailedError() = runTest { prepareToProvideDeviceIds() prepareForEncryption() whenever(nativeLib.prepareForLogin(primaryKey = primaryKey)).thenReturn(validLoginKeys) @@ -531,7 +532,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenProcessInvalidCodeThenReturnInvalidCodeError() { + fun whenProcessInvalidCodeThenReturnInvalidCodeError() = runTest { val result = syncRepo.processCode("invalidCode") as Error assertEquals(INVALID_CODE.code, result.code) @@ -622,7 +623,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenProcessConnectCodeFromAuthenticatedDeviceThenConnectsDevice() { + fun whenProcessConnectCodeFromAuthenticatedDeviceThenConnectsDevice() = runTest { givenAuthenticatedDevice() whenever(nativeLib.seal(jsonRecoveryKey, primaryKey)).thenReturn(encryptedRecoveryCode) whenever(syncApi.connect(token, deviceId, encryptedRecoveryCode)).thenReturn(Success(true)) @@ -634,7 +635,7 @@ class AppSyncAccountRepositoryTest { } @Test - fun whenProcessConnectCodeFromUnauthenticatedDeviceThenAccountCreatedAndConnects() { + fun whenProcessConnectCodeFromUnauthenticatedDeviceThenAccountCreatedAndConnects() = runTest { whenever(syncStore.primaryKey).thenReturn(primaryKey) whenever(syncStore.isSignedIn()).thenReturn(false).thenReturn(true) whenever(syncStore.userId).thenReturn(userId) diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/FakeSyncableDataProvider.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/FakeSyncableDataProvider.kt index e3f5d8354386..bfa63b241335 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/FakeSyncableDataProvider.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/FakeSyncableDataProvider.kt @@ -27,7 +27,7 @@ class FakeSyncableDataProvider( ) : SyncableDataProvider { override fun getType(): SyncableType = syncableType - override fun getChanges(): SyncChangesRequest { + override suspend fun getChanges(): SyncChangesRequest { return fakeChanges } } diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/SyncEngineTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/SyncEngineTest.kt index c2062913072d..97c921d387f1 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/SyncEngineTest.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/engine/SyncEngineTest.kt @@ -43,6 +43,7 @@ import com.duckduckgo.sync.store.model.SyncAttemptState.IN_PROGRESS import com.duckduckgo.sync.store.model.SyncAttemptState.SUCCESS import com.duckduckgo.sync.store.model.SyncOperationErrorType.ORPHANS_PRESENT import com.duckduckgo.sync.store.model.SyncOperationErrorType.TIMESTAMP_CONFLICT +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Ignore import org.junit.Test @@ -84,7 +85,7 @@ internal class SyncEngineTest { } @Test - fun whenFeatureReadTriggeredAndSyncIsDisabledNoSyncOperationIsTriggered() { + fun whenFeatureReadTriggeredAndSyncIsDisabledNoSyncOperationIsTriggered() = runTest { whenever(syncStore.isSignedIn()).thenReturn(false) syncEngine.triggerSync(FEATURE_READ) verifyNoInteractions(syncApiClient) @@ -93,7 +94,7 @@ internal class SyncEngineTest { } @Test - fun whenAppOpensAndSyncIsDisabledNoSyncOperationIsTriggered() { + fun whenAppOpensAndSyncIsDisabledNoSyncOperationIsTriggered() = runTest { whenever(syncStore.isSignedIn()).thenReturn(false) syncEngine.triggerSync(APP_OPEN) verifyNoInteractions(syncApiClient) @@ -102,7 +103,7 @@ internal class SyncEngineTest { } @Test - fun whenBackgroundSyncOperationTriggeredAndSyncIsDisabledNoSyncOperationIsTriggered() { + fun whenBackgroundSyncOperationTriggeredAndSyncIsDisabledNoSyncOperationIsTriggered() = runTest { whenever(syncStore.isSignedIn()).thenReturn(false) syncEngine.triggerSync(BACKGROUND_SYNC) verifyNoInteractions(syncApiClient) @@ -111,7 +112,7 @@ internal class SyncEngineTest { } @Test - fun whenDataChangesAndSyncIsDisabledNoSyncOperationIsTriggered() { + fun whenDataChangesAndSyncIsDisabledNoSyncOperationIsTriggered() = runTest { whenever(syncStore.isSignedIn()).thenReturn(false) syncEngine.triggerSync(DATA_CHANGE) verifyNoInteractions(syncApiClient) @@ -120,14 +121,14 @@ internal class SyncEngineTest { } @Test - fun whenCreatingSyncAccountAndNoLocalChangesThenNothingIsSent() { + fun whenCreatingSyncAccountAndNoLocalChangesThenNothingIsSent() = runTest { syncEngine.triggerSync(ACCOUNT_CREATION) verifyNoInteractions(syncApiClient) } @Test - fun whenCreatingSyncAccountThenDataIsSentAndStateUpdatedWithSuccess() { + fun whenCreatingSyncAccountThenDataIsSentAndStateUpdatedWithSuccess() = runTest { givenLocalChanges() givenPatchSuccess() @@ -139,7 +140,7 @@ internal class SyncEngineTest { } @Test - fun whenSyncingDataIsDisabledThenNoSyncOperationIsTriggered() { + fun whenSyncingDataIsDisabledThenNoSyncOperationIsTriggered() = runTest { whenever(syncStore.isSignedIn()).thenReturn(true) whenever(syncStore.syncingDataEnabled).thenReturn(false) @@ -155,7 +156,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenCreatingSyncAccountThenDataIsSentAndStateUpdatedWithError() { + fun whenCreatingSyncAccountThenDataIsSentAndStateUpdatedWithError() = runTest { givenLocalChanges() givenPatchError() @@ -167,7 +168,7 @@ internal class SyncEngineTest { } @Test - fun whenAppOpenWithoutChangesAndGetRemoteSucceedsThenStateIsUpdated() { + fun whenAppOpenWithoutChangesAndGetRemoteSucceedsThenStateIsUpdated() = runTest { givenNoLocalChanges() givenGetSuccess() @@ -180,7 +181,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenAppOpenWithoutChangesAndGetRemoteFailsThenStateIsUpdated() { + fun whenAppOpenWithoutChangesAndGetRemoteFailsThenStateIsUpdated() = runTest { givenNoLocalChanges() givenGetError() @@ -191,7 +192,7 @@ internal class SyncEngineTest { } @Test - fun whenAppOpenWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() { + fun whenAppOpenWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() = runTest { givenLocalChanges() givenPatchSuccess() @@ -202,7 +203,7 @@ internal class SyncEngineTest { } @Test - fun whenAppOpenWithChangesAndFeatureFirstSyncThenPerformGetAndPatch() { + fun whenAppOpenWithChangesAndFeatureFirstSyncThenPerformGetAndPatch() = runTest { givenFirstSyncLocalChanges() givenGetSuccess() givenPatchSuccess() @@ -217,7 +218,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenAppOpenWithChangesAndPatchRemoteFailsThenStateIsUpdated() { + fun whenAppOpenWithChangesAndPatchRemoteFailsThenStateIsUpdated() = runTest { givenLocalChanges() givenPatchError() @@ -228,7 +229,7 @@ internal class SyncEngineTest { } @Test - fun whenFeatureReadWithoutChangesAndGetRemoteSucceedsThenStateIsUpdated() { + fun whenFeatureReadWithoutChangesAndGetRemoteSucceedsThenStateIsUpdated() = runTest { givenNoLocalChanges() givenGetSuccess() @@ -241,7 +242,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenFeatureReadWithoutChangesAndGetRemoteFailsThenStateIsUpdated() { + fun whenFeatureReadWithoutChangesAndGetRemoteFailsThenStateIsUpdated() = runTest { givenNoLocalChanges() givenGetError() @@ -252,7 +253,7 @@ internal class SyncEngineTest { } @Test - fun whenFeatureReadWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() { + fun whenFeatureReadWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() = runTest { givenLocalChanges() givenPatchSuccess() @@ -265,7 +266,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenFeatureReadWithChangesAndPatchRemoteFailsThenStateIsUpdated() { + fun whenFeatureReadWithChangesAndPatchRemoteFailsThenStateIsUpdated() = runTest { givenLocalChanges() givenPatchError() @@ -276,7 +277,7 @@ internal class SyncEngineTest { } @Test - fun whenDataChangeWithoutChangesAndGetRemoteSucceedsThenStateIsUpdated() { + fun whenDataChangeWithoutChangesAndGetRemoteSucceedsThenStateIsUpdated() = runTest { givenNoLocalChanges() givenGetSuccess() @@ -289,7 +290,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenDataChangeWithoutChangesAndGetRemoteFailsThenStateIsUpdated() { + fun whenDataChangeWithoutChangesAndGetRemoteFailsThenStateIsUpdated() = runTest { givenNoLocalChanges() givenGetError() @@ -300,7 +301,7 @@ internal class SyncEngineTest { } @Test - fun whenDataChangeWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() { + fun whenDataChangeWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() = runTest { givenLocalChanges() givenPatchSuccess() @@ -311,7 +312,7 @@ internal class SyncEngineTest { } @Test - fun whenDataChangeWithChangesForFirstSyncThenStateIsUpdated() { + fun whenDataChangeWithChangesForFirstSyncThenStateIsUpdated() = runTest { givenFirstSyncLocalChanges() givenPatchSuccess() givenGetSuccess() @@ -326,7 +327,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenDataChangeWithChangesAndPatchRemoteFailsThenStateIsUpdated() { + fun whenDataChangeWithChangesAndPatchRemoteFailsThenStateIsUpdated() = runTest { givenLocalChanges() givenPatchError() @@ -337,7 +338,7 @@ internal class SyncEngineTest { } @Test - fun whenBackgroundSyncCantBeScheduledThenNothingHappens() { + fun whenBackgroundSyncCantBeScheduledThenNothingHappens() = runTest { whenever(syncScheduler.scheduleOperation()).thenReturn(DISCARD) verifyNoInteractions(syncApiClient) @@ -345,7 +346,7 @@ internal class SyncEngineTest { } @Test - fun whenBackgroundSyncWithoutChangesAndGetRemoteSucceedsThenStateIsUpdated() { + fun whenBackgroundSyncWithoutChangesAndGetRemoteSucceedsThenStateIsUpdated() = runTest { whenever(syncScheduler.scheduleOperation()).thenReturn(EXECUTE) givenNoLocalChanges() givenGetSuccess() @@ -359,7 +360,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenBackgroundSyncWithoutChangesAndGetRemoteFailsThenStateIsUpdated() { + fun whenBackgroundSyncWithoutChangesAndGetRemoteFailsThenStateIsUpdated() = runTest { whenever(syncScheduler.scheduleOperation()).thenReturn(EXECUTE) givenNoLocalChanges() givenGetError() @@ -371,7 +372,7 @@ internal class SyncEngineTest { } @Test - fun whenBackgroundSyncWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() { + fun whenBackgroundSyncWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() = runTest { whenever(syncScheduler.scheduleOperation()).thenReturn(EXECUTE) givenLocalChanges() givenPatchSuccess() @@ -383,7 +384,7 @@ internal class SyncEngineTest { } @Test - fun whenFirstSyncBackgroundSyncWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() { + fun whenFirstSyncBackgroundSyncWithChangesAndPatchRemoteSucceedsThenStateIsUpdated() = runTest { whenever(syncScheduler.scheduleOperation()).thenReturn(EXECUTE) givenFirstSyncLocalChanges() givenPatchSuccess() @@ -399,7 +400,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenBackgroundSyncWithChangesAndPatchRemoteFailsThenStateIsUpdated() { + fun whenBackgroundSyncWithChangesAndPatchRemoteFailsThenStateIsUpdated() = runTest { whenever(syncScheduler.scheduleOperation()).thenReturn(EXECUTE) givenLocalChanges() givenPatchError() @@ -413,7 +414,7 @@ internal class SyncEngineTest { @Test @Ignore // https://app.asana.com/0/1204842202586359/1205158805627400/f - fun whenAccountLoginGetRemoteFailsThenStateIsUpdated() { + fun whenAccountLoginGetRemoteFailsThenStateIsUpdated() = runTest { givenLocalChanges() givenGetError() @@ -424,7 +425,7 @@ internal class SyncEngineTest { } @Test - fun whenAccountLoginSucceedsThenStateIsUpdated() { + fun whenAccountLoginSucceedsThenStateIsUpdated() = runTest { givenFirstSyncLocalChanges() givenPatchSuccess() givenGetSuccess() @@ -437,14 +438,14 @@ internal class SyncEngineTest { } @Test - fun whenTriggeringSyncAndSyncAlreadyInProgressThenSyncIsDismissed() { + fun whenTriggeringSyncAndSyncAlreadyInProgressThenSyncIsDismissed() = runTest { whenever(syncStateRepository.current()).thenReturn(SyncAttempt(state = IN_PROGRESS)) syncEngine.triggerSync(DATA_CHANGE) verifyNoInteractions(syncApiClient) } @Test - fun whenPatchNewDataFailsBecauseCountLimitThenNotifyFeature() { + fun whenPatchNewDataFailsBecauseCountLimitThenNotifyFeature() = runTest { givenLocalChanges() givenPatchLimitError() val persisterPluginMock = mock() @@ -456,7 +457,7 @@ internal class SyncEngineTest { } @Test - fun whenPatchNewDataFailsBecauseContentTooLargeThenNotifyFeature() { + fun whenPatchNewDataFailsBecauseContentTooLargeThenNotifyFeature() = runTest { givenLocalChanges() givenPatchContentTooLargeError() val persisterPluginMock = mock() @@ -468,7 +469,7 @@ internal class SyncEngineTest { } @Test - fun whenPatchNewDataFailsBecauseNonFeatureErrorThenDoNotNotifyFeature() { + fun whenPatchNewDataFailsBecauseNonFeatureErrorThenDoNotNotifyFeature() = runTest { givenLocalChanges() givenPatchError() val persisterPluginMock = mock() @@ -480,7 +481,7 @@ internal class SyncEngineTest { } @Test - fun whenSyncTriggeredDailyPixelIsSent() { + fun whenSyncTriggeredDailyPixelIsSent() = runTest { givenLocalChanges() givenPatchSuccess() @@ -493,7 +494,7 @@ internal class SyncEngineTest { } @Test - fun whenSyncTriggeredWithChangesAndPatchRemoteSucceedsWithTimestampConflictThenStateIsUpdatedAndPixelIsFired() { + fun whenSyncTriggeredWithChangesAndPatchRemoteSucceedsWithTimestampConflictThenStateIsUpdatedAndPixelIsFired() = runTest { givenLocalChangesWithTimestampConflict() givenPatchSuccess() @@ -507,7 +508,7 @@ internal class SyncEngineTest { } @Test - fun whenSyncTriggeredWithChangesAndPatchRemoteSucceedsWithOrphansThenStateIsUpdatedAndPixelIsFired() { + fun whenSyncTriggeredWithChangesAndPatchRemoteSucceedsWithOrphansThenStateIsUpdatedAndPixelIsFired() = runTest { givenLocalChangesWithOrphansPresent() givenPatchSuccess() diff --git a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt index 5ecfe0dd5ddd..37805d43b9aa 100644 --- a/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt +++ b/sync/sync-impl/src/test/java/com/duckduckgo/sync/impl/ui/SyncWithAnotherDeviceViewModelTest.kt @@ -386,7 +386,7 @@ class SyncWithAnotherDeviceViewModelTest { } } - private fun configureExchangeKeysSupported(): Pair { + private suspend fun configureExchangeKeysSupported(): Pair { syncFeature.exchangeKeysToSyncWithAnotherDevice().setRawStoredState(State(true)) whenever(syncRepository.pollSecondDeviceExchangeAcknowledgement()).thenReturn(Success(true)) whenever(syncRepository.getCodeType(any())).thenReturn(EXCHANGE) diff --git a/sync/sync-settings-api/src/main/java/com/duckduckgo/sync/settings/api/SyncableSetting.kt b/sync/sync-settings-api/src/main/java/com/duckduckgo/sync/settings/api/SyncableSetting.kt index 23e2403bfa31..b6ef954b310c 100644 --- a/sync/sync-settings-api/src/main/java/com/duckduckgo/sync/settings/api/SyncableSetting.kt +++ b/sync/sync-settings-api/src/main/java/com/duckduckgo/sync/settings/api/SyncableSetting.kt @@ -30,21 +30,21 @@ interface SyncableSetting { * Setting value represented as a string. * If the setting is not set or has no value, null should be returned. */ - fun getValue(): String? + suspend fun getValue(): String? /** * Should save a new value for the setting. * @param value the new value to be stored in the local store. If value not present or deleted, will be null. * @return true if local value was changed, false otherwise. */ - fun save(value: String?): Boolean + suspend fun save(value: String?): Boolean /** * Should provide a way to merge remote value with local value on First sync (Deduplication). * @param value the remote value to be merged with the local value. If remote value not present or deleted, will be null. * @return true if local value was changed, false otherwise. */ - fun deduplicate(value: String?): Boolean + suspend fun deduplicate(value: String?): Boolean /** * Should provide a way to register to changes applied by Sync Service. diff --git a/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersister.kt b/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersister.kt index a76369d7978a..71f21a27c789 100644 --- a/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersister.kt +++ b/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersister.kt @@ -107,7 +107,7 @@ class SettingsSyncDataPersister @Inject constructor( return result } - private fun processEntries( + private suspend fun processEntries( settings: SettingsSyncEntries, syncableSettings: Collection, conflictResolution: SyncConflictResolution, @@ -141,7 +141,7 @@ class SettingsSyncDataPersister @Inject constructor( return Success() } - private fun applyChanges(syncableFeature: SyncableSetting, entry: SettingEntryResponse): SyncMergeResult { + private suspend fun applyChanges(syncableFeature: SyncableSetting, entry: SettingEntryResponse): SyncMergeResult { val localCredential = settingsSyncMetadataDao.get(entry.key) val clientModifiedSinceMillis = runCatching { DatabaseDateFormatter.parseIso8601ToMillis(syncSettingsSyncStore.startTimeStamp) }.getOrDefault(0) diff --git a/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataProvider.kt b/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataProvider.kt index 2933dbfe23f6..2fd0a808c945 100644 --- a/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataProvider.kt +++ b/sync/sync-settings-impl/src/main/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataProvider.kt @@ -41,7 +41,7 @@ class SettingsSyncDataProvider @Inject constructor( ) : SyncableDataProvider { override fun getType(): SyncableType = SETTINGS - override fun getChanges(): SyncChangesRequest { + override suspend fun getChanges(): SyncChangesRequest { val syncableSettings = syncableSettings.getPlugins() if (settingsSyncStore.serverModifiedSince == "0") { val keys = syncableSettings.map { it.key } @@ -55,7 +55,7 @@ class SettingsSyncDataProvider @Inject constructor( return formatUpdates(updates) } - private fun getUpdatesSince( + private suspend fun getUpdatesSince( syncableSettings: Collection, clientModifiedSince: String, ): List { @@ -107,7 +107,7 @@ class SettingsSyncDataProvider @Inject constructor( } } - private fun SyncableSetting.asSettingEntry(clientModifiedSince: String): SettingEntry { + private suspend fun SyncableSetting.asSettingEntry(clientModifiedSince: String): SettingEntry { val value = getValue()?.let { syncCrypto.encrypt(it) } return SettingEntry( key = key, diff --git a/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/FakeSyncableSetting.kt b/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/FakeSyncableSetting.kt index cc976018ff0c..9571bc8e761a 100644 --- a/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/FakeSyncableSetting.kt +++ b/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/FakeSyncableSetting.kt @@ -25,14 +25,14 @@ open class FakeSyncableSetting : SyncableSetting { private var value: String? = "fake_value" - override fun getValue(): String? = value + override suspend fun getValue(): String? = value - override fun save(value: String?): Boolean { + override suspend fun save(value: String?): Boolean { this.value = value return true } - override fun deduplicate(value: String?): Boolean { + override suspend fun deduplicate(value: String?): Boolean { this.value = value return true } diff --git a/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersisterTest.kt b/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersisterTest.kt index 25a565e0f7ee..c6190dc2759b 100644 --- a/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersisterTest.kt +++ b/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataPersisterTest.kt @@ -28,6 +28,7 @@ import com.duckduckgo.sync.api.engine.SyncMergeResult.Success import com.duckduckgo.sync.api.engine.SyncableDataPersister.SyncConflictResolution.DEDUPLICATION import com.duckduckgo.sync.api.engine.SyncableDataPersister.SyncConflictResolution.TIMESTAMP import kotlinx.coroutines.* +import kotlinx.coroutines.test.runTest import org.junit.* import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -67,7 +68,7 @@ class SettingsSyncDataPersisterTest { } @Test - fun whenPersistChangesDeduplicationWithdValueThenCallDeduplicateWithValue() { + fun whenPersistChangesDeduplicationWithdValueThenCallDeduplicateWithValue() = runTest { val result = testee.onSuccess( changes = SyncChangesResponse( type = SyncableType.SETTINGS, @@ -81,7 +82,7 @@ class SettingsSyncDataPersisterTest { } @Test - fun whenPersistChangesDeduplicationWithDeletedValueThenCallDeduplicateWithNull() { + fun whenPersistChangesDeduplicationWithDeletedValueThenCallDeduplicateWithNull() = runTest { val result = testee.onSuccess( changes = SyncChangesResponse( type = SyncableType.SETTINGS, @@ -95,7 +96,7 @@ class SettingsSyncDataPersisterTest { } @Test - fun whenPersistChangesTimestampAndNoRecentChangeThenCallMergeWithValue() { + fun whenPersistChangesTimestampAndNoRecentChangeThenCallMergeWithValue() = runTest { settingSyncStore.startTimeStamp = "2023-08-31T10:06:16.022Z" val result = testee.onSuccess( changes = SyncChangesResponse( @@ -110,7 +111,7 @@ class SettingsSyncDataPersisterTest { } @Test - fun whenPersistChangesTimestampWithDeletedValueThenCallSaveWithNull() { + fun whenPersistChangesTimestampWithDeletedValueThenCallSaveWithNull() = runTest { val result = testee.onSuccess( changes = SyncChangesResponse( type = SyncableType.SETTINGS, @@ -124,7 +125,7 @@ class SettingsSyncDataPersisterTest { } @Test - fun whenPersistChangesTimestampButRecentlyModifiedThenSkip() { + fun whenPersistChangesTimestampButRecentlyModifiedThenSkip() = runTest { settingSyncStore.startTimeStamp = "2023-08-31T10:06:16.022Z" metadataDao.addOrUpdate( SettingsSyncMetadataEntity( diff --git a/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataProviderTest.kt b/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataProviderTest.kt index 5feed39f78b5..d0bc3bf80feb 100644 --- a/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataProviderTest.kt +++ b/sync/sync-settings-impl/src/test/java/com/duckduckgo/sync/settings/impl/SettingsSyncDataProviderTest.kt @@ -96,7 +96,7 @@ class SettingsSyncDataProviderTest { } @Test - fun whenGetChangesForFirstSyncThenChangesIncludeAllValues() { + fun whenGetChangesForFirstSyncThenChangesIncludeAllValues() = runTest { val changes = testee.getChanges() assertTrue(changes.type == SyncableType.SETTINGS) @@ -111,7 +111,7 @@ class SettingsSyncDataProviderTest { @Test @Ignore("Need to decide strategy first") - fun whenGetChangesSubsequentCallsWithNewValueThenIncludeNewValues() { + fun whenGetChangesSubsequentCallsWithNewValueThenIncludeNewValues() = runTest { settingSyncStore.serverModifiedSince = "2022-01-01T00:00:00Z" settingSyncStore.clientModifiedSince = "2022-01-01T00:00:00Z" @@ -128,7 +128,7 @@ class SettingsSyncDataProviderTest { } @Test - fun whenGetChangesSubsequentCallsAndNoChangesThenUpdatesAreEmpty() { + fun whenGetChangesSubsequentCallsAndNoChangesThenUpdatesAreEmpty() = runTest { settingSyncStore.serverModifiedSince = "2022-01-01T00:00:00Z" settingSyncStore.clientModifiedSince = "2022-01-01T00:00:00Z" metadataDao.addOrUpdate(SettingsSyncMetadataEntity(duckAddressSetting.key, "2022-01-01T00:00:00Z", "")) @@ -141,7 +141,7 @@ class SettingsSyncDataProviderTest { } @Test - fun whenDBHasDataButItIsFirstSyncThenIncludeAllValues() { + fun whenDBHasDataButItIsFirstSyncThenIncludeAllValues() = runTest { metadataDao.addOrUpdate(SettingsSyncMetadataEntity(duckAddressSetting.key, "2022-01-01T00:00:00Z", "")) val changes = testee.getChanges() @@ -157,7 +157,7 @@ class SettingsSyncDataProviderTest { } @Test - fun whenGetChangesForFirstSyncAndSettingNullThenSendAsDeleted() { + fun whenGetChangesForFirstSyncAndSettingNullThenSendAsDeleted() = runTest { duckAddressSetting.save(null) val changes = testee.getChanges() @@ -173,7 +173,7 @@ class SettingsSyncDataProviderTest { } @Test - fun whenGetChangesSubsequentCallsAndSettingNullThenSendAsDeleted() { + fun whenGetChangesSubsequentCallsAndSettingNullThenSendAsDeleted() = runTest { settingSyncStore.serverModifiedSince = "2022-01-01T00:00:00Z" settingSyncStore.clientModifiedSince = "2022-01-01T00:00:00Z" metadataDao.addOrUpdate(SettingsSyncMetadataEntity(duckAddressSetting.key, "2022-01-02T00:00:00Z", "")) @@ -192,7 +192,7 @@ class SettingsSyncDataProviderTest { } @Test - fun whenSyncableSettingNotFoundThenSkipUpdate() { + fun whenSyncableSettingNotFoundThenSkipUpdate() = runTest { settingSyncStore.serverModifiedSince = "2022-01-01T00:00:00Z" settingSyncStore.clientModifiedSince = "2022-01-01T00:00:00Z" metadataDao.addOrUpdate(SettingsSyncMetadataEntity("unknown_setting", "2022-01-02T00:00:00Z", "")) diff --git a/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/RealVoiceStateReporterPlugin.kt b/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/RealVoiceStateReporterPlugin.kt index 23a6d6dc2ef9..4691c8f5da8f 100644 --- a/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/RealVoiceStateReporterPlugin.kt +++ b/voice-search/voice-search-impl/src/main/java/com/duckduckgo/voice/impl/RealVoiceStateReporterPlugin.kt @@ -32,7 +32,7 @@ interface VoiceStateReporterPlugin class RealVoiceStateReporterPlugin @Inject constructor( private val voiceSearchAvailability: VoiceSearchAvailability, ) : VoiceStateReporterPlugin, BrowserFeatureStateReporterPlugin { - override fun featureStateParams(): Map { + override suspend fun featureStateParams(): Map { return mapOf(PixelParameter.VOICE_SEARCH to voiceSearchAvailability.isVoiceSearchAvailable.toBinaryString()) } }