Skip to content

Commit ca94b7b

Browse files
authored
Fix: NTP creation crash with tab swiping (#6007)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1207418217763355/task/1210095537840719?focus=true ### Description This PR fixes the NTP creation. There was a logical error in the previous implementation when the wrong boolean was being returned in `transformWhile()` when requesting a new tab, which in some situation might result in the flow completion without emitting any value, which resulted in a crash. I added code that throws an exception when a new tab is not created because it's an invalid state in the DB that I'm not sure how we should recover from. I don't think that's what was happening but in case it does, we'll know where to start looking. ### Steps to test this PR I wasn't able to reproduce it, however, I added unit tests that should verify the fix works as expected.
1 parent c3e2378 commit ca94b7b

File tree

2 files changed

+99
-7
lines changed

2 files changed

+99
-7
lines changed

app/src/main/java/com/duckduckgo/app/browser/tabs/DefaultTabManager.kt

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,27 @@ package com.duckduckgo.app.browser.tabs
1818

1919
import com.duckduckgo.app.browser.SkipUrlConversionOnNewTabFeature
2020
import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
21+
import com.duckduckgo.app.browser.tabs.TabManager.Companion.NEW_TAB_CREATION_TIMEOUT_LIMIT
2122
import com.duckduckgo.app.tabs.model.TabEntity
2223
import com.duckduckgo.app.tabs.model.TabRepository
2324
import com.duckduckgo.common.utils.DispatcherProvider
2425
import com.duckduckgo.di.scopes.ActivityScope
2526
import com.squareup.anvil.annotations.ContributesBinding
2627
import javax.inject.Inject
27-
import kotlinx.coroutines.flow.first
28+
import kotlin.time.Duration.Companion.seconds
29+
import kotlinx.coroutines.FlowPreview
30+
import kotlinx.coroutines.TimeoutCancellationException
31+
import kotlinx.coroutines.flow.catch
32+
import kotlinx.coroutines.flow.firstOrNull
33+
import kotlinx.coroutines.flow.timeout
2834
import kotlinx.coroutines.flow.transformWhile
2935
import kotlinx.coroutines.withContext
3036
import timber.log.Timber
3137

3238
interface TabManager {
3339
companion object {
3440
const val MAX_ACTIVE_TABS = 20
41+
const val NEW_TAB_CREATION_TIMEOUT_LIMIT = 2 // seconds
3542
}
3643

3744
fun registerCallbacks(onTabsUpdated: (List<String>) -> Unit)
@@ -76,15 +83,27 @@ class DefaultTabManager @Inject constructor(
7683
}
7784
}
7885

86+
@OptIn(FlowPreview::class)
7987
override suspend fun requestAndWaitForNewTab(): TabEntity = withContext(dispatchers.io()) {
8088
val tabId = openNewTab()
81-
return@withContext tabRepository.flowTabs.transformWhile { result ->
82-
result.firstOrNull { it.tabId == tabId }?.let { entity ->
83-
emit(entity)
84-
return@transformWhile true
89+
return@withContext tabRepository.flowTabs
90+
.transformWhile { result ->
91+
result.firstOrNull { it.tabId == tabId }?.let { entity ->
92+
emit(entity)
93+
return@transformWhile false // stop after finding the tab
94+
}
95+
return@transformWhile true // continue looking if not found
8596
}
86-
return@transformWhile false
87-
}.first()
97+
.timeout(NEW_TAB_CREATION_TIMEOUT_LIMIT.seconds)
98+
.catch { e ->
99+
if (e is TimeoutCancellationException) {
100+
// timeout expired and the new tab was not found
101+
throw IllegalStateException("A new tab failed to be created within $NEW_TAB_CREATION_TIMEOUT_LIMIT second")
102+
} else {
103+
throw e
104+
}
105+
}
106+
.firstOrNull() ?: throw IllegalStateException("Tabs flow completed before finding the new tab")
88107
}
89108

90109
override suspend fun switchToTab(tabId: String) = withContext(dispatchers.io()) {

app/src/test/java/com/duckduckgo/app/browser/tabs/DefaultTabManagerTest.kt

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ package com.duckduckgo.app.browser.tabs
33
import androidx.test.ext.junit.runners.AndroidJUnit4
44
import com.duckduckgo.app.browser.SkipUrlConversionOnNewTabFeature
55
import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
6+
import com.duckduckgo.app.browser.tabs.TabManager.Companion.NEW_TAB_CREATION_TIMEOUT_LIMIT
67
import com.duckduckgo.app.tabs.model.TabEntity
78
import com.duckduckgo.app.tabs.model.TabRepository
89
import com.duckduckgo.common.test.CoroutineTestRule
910
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
1011
import com.duckduckgo.feature.toggles.api.Toggle.State
12+
import kotlinx.coroutines.delay
13+
import kotlinx.coroutines.flow.flow
1114
import kotlinx.coroutines.flow.flowOf
1215
import kotlinx.coroutines.test.runTest
1316
import org.junit.Assert.assertEquals
17+
import org.junit.Assert.fail
1418
import org.junit.Before
1519
import org.junit.Rule
1620
import org.junit.Test
@@ -164,4 +168,73 @@ class DefaultTabManagerTest {
164168
verify(tabRepository).add(url = query, skipHome = false)
165169
verify(omnibarEntryConverter, never()).convertQueryToUrl(query)
166170
}
171+
172+
@Test
173+
fun whenRequestAndWaitForNewTabAndTabIsFoundImmediatelyThenReturnTabEntity() = runTest {
174+
val tabId = "tabId"
175+
val tabEntity = TabEntity(tabId = tabId, position = 0)
176+
whenever(tabRepository.flowTabs).thenReturn(flowOf(listOf(tabEntity)))
177+
whenever(tabRepository.add()).thenReturn(tabId)
178+
179+
val result = testee.requestAndWaitForNewTab()
180+
181+
assertEquals(tabEntity, result)
182+
}
183+
184+
@Test
185+
fun whenRequestAndWaitForNewTabAndTabIsFoundAfterDelayThenReturnTabEntity() = runTest {
186+
val tabId = "tabId"
187+
val tabEntity = TabEntity(tabId = tabId, position = 0)
188+
189+
val flow = flow {
190+
emit(emptyList())
191+
delay(500) // Delay less than the timeout
192+
emit(listOf(tabEntity))
193+
}
194+
whenever(tabRepository.flowTabs).thenReturn(flow)
195+
whenever(tabRepository.add()).thenReturn(tabId)
196+
197+
val result = testee.requestAndWaitForNewTab()
198+
199+
assertEquals(tabEntity, result)
200+
}
201+
202+
@Test
203+
fun whenRequestAndWaitForNewTabAndTimeoutOccursThenThrowException() = runTest {
204+
val tabId = "tabId"
205+
206+
val flow = flow {
207+
while (true) {
208+
emit(emptyList<TabEntity>())
209+
delay(300)
210+
}
211+
}
212+
whenever(tabRepository.flowTabs).thenReturn(flow)
213+
whenever(tabRepository.add()).thenReturn(tabId)
214+
215+
try {
216+
testee.requestAndWaitForNewTab()
217+
fail("Expected IllegalStateException was not thrown")
218+
} catch (e: IllegalStateException) {
219+
assertEquals("A new tab failed to be created within $NEW_TAB_CREATION_TIMEOUT_LIMIT second", e.message)
220+
}
221+
}
222+
223+
@Test
224+
fun whenRequestAndWaitForNewTabAndFlowCompletesWithoutFindingTabThenThrowException() = runTest {
225+
val tabId = "tabId"
226+
val otherTabId = "otherTabId"
227+
val otherTabEntity = TabEntity(tabId = otherTabId, position = 0)
228+
229+
val flow = flowOf(listOf(otherTabEntity))
230+
whenever(tabRepository.flowTabs).thenReturn(flow)
231+
whenever(tabRepository.add()).thenReturn(tabId)
232+
233+
try {
234+
testee.requestAndWaitForNewTab()
235+
fail("Expected IllegalStateException was not thrown")
236+
} catch (e: IllegalStateException) {
237+
assertEquals("Tabs flow completed before finding the new tab", e.message)
238+
}
239+
}
167240
}

0 commit comments

Comments
 (0)