Skip to content

Commit 4e2f5d5

Browse files
Hatch: Refactor NtpAfterIdleManager to classify per-render correctly and fix hatch pixels on later NTPs
1 parent 3bb7c45 commit 4e2f5d5

18 files changed

Lines changed: 439 additions & 52 deletions

File tree

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3525,7 +3525,7 @@ class BrowserTabFragment :
35253525
newTabReturnHatchView.setHatchListener(
35263526
object : NewTabReturnHatchView.HatchListener {
35273527
override fun onHatchPressed() {
3528-
ntpAfterIdleManager.fireReturnToPageTapped()
3528+
ntpAfterIdleManager.onReturnToPageTapped()
35293529
browserActivity?.openExistingTab(newTabReturnHatchView.tabId)
35303530
}
35313531

app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
345345
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.MALWARE
346346
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.PHISHING
347347
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.SCAM
348+
import com.duckduckgo.newtabpage.api.NtpAfterIdleManager
348349
import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels
349350
import com.duckduckgo.privacy.config.api.AmpLinkInfo
350351
import com.duckduckgo.privacy.config.api.AmpLinks
@@ -516,6 +517,7 @@ class BrowserTabViewModel @Inject constructor(
516517
private val browserUiLockFeature: BrowserUiLockFeature,
517518
private val progressBarUpgradeFeature: ProgressBarUpgradeFeature,
518519
private val faviconFetchingFixFeature: FaviconFetchingFixFeature,
520+
private val ntpAfterIdleManager: NtpAfterIdleManager,
519521
) : ViewModel(),
520522
WebViewClientListener,
521523
EditSavedSiteListener,
@@ -1274,11 +1276,16 @@ class BrowserTabViewModel @Inject constructor(
12741276
return
12751277
}
12761278

1277-
if (currentGlobalLayoutState() is Invalidated) {
1279+
val layoutState = currentGlobalLayoutState()
1280+
if (layoutState is Invalidated) {
12781281
recoverTabWithQuery(query)
12791282
return
12801283
}
12811284

1285+
if (androidBrowserConfig.showNTPAfterIdleReturn().isEnabled() && layoutState is Browser && layoutState.isNewTabState) {
1286+
ntpAfterIdleManager.onNtpSearchSubmitted()
1287+
}
1288+
12821289
val cta = currentCtaViewState().cta
12831290

12841291
if (cta is OnboardingDaxDialogCta) {

app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import com.duckduckgo.app.pixels.AppPixelName.APP_RATING_DIALOG_SHOWN
5555
import com.duckduckgo.app.pixels.AppPixelName.APP_RATING_DIALOG_USER_CANCELLED
5656
import com.duckduckgo.app.pixels.AppPixelName.APP_RATING_DIALOG_USER_DECLINED_RATING
5757
import com.duckduckgo.app.pixels.AppPixelName.APP_RATING_DIALOG_USER_GAVE_RATING
58+
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
5859
import com.duckduckgo.app.statistics.pixels.Pixel
5960
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter
6061
import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
@@ -67,6 +68,7 @@ import com.duckduckgo.di.scopes.AppScope
6768
import com.duckduckgo.duckchat.api.DuckAiFeatureState
6869
import com.duckduckgo.feature.toggles.api.Toggle
6970
import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue
71+
import com.duckduckgo.newtabpage.api.NtpAfterIdleManager
7072
import kotlinx.coroutines.CoroutineScope
7173
import kotlinx.coroutines.FlowPreview
7274
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
@@ -77,9 +79,12 @@ import kotlinx.coroutines.flow.SharingStarted
7779
import kotlinx.coroutines.flow.combine
7880
import kotlinx.coroutines.flow.debounce
7981
import kotlinx.coroutines.flow.distinctUntilChanged
82+
import kotlinx.coroutines.flow.filter
8083
import kotlinx.coroutines.flow.filterNot
8184
import kotlinx.coroutines.flow.filterNotNull
85+
import kotlinx.coroutines.flow.launchIn
8286
import kotlinx.coroutines.flow.map
87+
import kotlinx.coroutines.flow.onEach
8388
import kotlinx.coroutines.flow.receiveAsFlow
8489
import kotlinx.coroutines.flow.stateIn
8590
import kotlinx.coroutines.flow.update
@@ -104,8 +109,21 @@ class BrowserViewModel @Inject constructor(
104109
private val additionalDefaultBrowserPrompts: AdditionalDefaultBrowserPrompts,
105110
private val swipingTabsFeature: SwipingTabsFeatureProvider,
106111
private val duckAiFeatureState: DuckAiFeatureState,
112+
private val ntpAfterIdleManager: NtpAfterIdleManager,
113+
private val androidBrowserConfigFeature: AndroidBrowserConfigFeature,
107114
) : ViewModel(), CoroutineScope {
108115

116+
init {
117+
if (androidBrowserConfigFeature.showNTPAfterIdleReturn().isEnabled()) {
118+
tabRepository.flowSelectedTab
119+
.map { tab -> tab?.let { it.tabId to it.url.isNullOrBlank() } }
120+
.distinctUntilChanged()
121+
.filter { it?.second == true }
122+
.onEach { ntpAfterIdleManager.onNtpShown() }
123+
.launchIn(viewModelScope)
124+
}
125+
}
126+
109127
override val coroutineContext: CoroutineContext
110128
get() = dispatchers.main()
111129

app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarLayoutViewModel.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ import com.duckduckgo.duckchat.api.DuckAiFeatureState
6767
import com.duckduckgo.duckchat.api.DuckChat
6868
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixelName
6969
import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
70-
import com.duckduckgo.newtabpage.api.NtpAfterIdleManager
7170
import com.duckduckgo.privacy.dashboard.impl.pixels.PrivacyDashboardPixels
7271
import com.duckduckgo.serp.logos.api.SerpEasterEggLogosToggles
7372
import com.duckduckgo.serp.logos.api.SerpLogo
@@ -114,7 +113,6 @@ class OmnibarLayoutViewModel @Inject constructor(
114113
private val addressBarTrackersAnimationManager: AddressBarTrackersAnimationManager,
115114
private val standardizedLeadingIconToggle: StandardizedLeadingIconFeatureToggle,
116115
private val progressBarUpgradeFeature: ProgressBarUpgradeFeature,
117-
private val ntpAfterIdleRepository: NtpAfterIdleManager,
118116
) : ViewModel() {
119117

120118
private val isSplitOmnibarEnabled = settingsDataStore.omnibarType == OmnibarType.SPLIT
@@ -965,9 +963,6 @@ class OmnibarLayoutViewModel @Inject constructor(
965963
AppPixelName.KEYBOARD_GO_SERP_CLICKED,
966964
AppPixelName.KEYBOARD_GO_WEBSITE_CLICKED,
967965
)
968-
if (_viewState.value.url.isEmpty()) {
969-
ntpAfterIdleRepository.fireBarUsedFromNtp()
970-
}
971966
}
972967

973968
fun onAnimationStarted(decoration: Decoration) {

app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/FirstScreenHandler.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import com.duckduckgo.app.settings.db.SettingsDataStore
2222
import com.duckduckgo.browser.api.BrowserLifecycleObserver
2323
import com.duckduckgo.common.utils.DispatcherProvider
2424
import com.duckduckgo.di.scopes.AppScope
25-
import com.duckduckgo.newtabpage.api.NtpAfterIdleManager
2625
import com.squareup.anvil.annotations.ContributesMultibinding
2726
import dagger.SingleInstanceIn
2827
import kotlinx.coroutines.CoroutineScope
@@ -41,7 +40,6 @@ class FirstScreenHandlerImpl @Inject constructor(
4140
private val settingsDataStore: SettingsDataStore,
4241
private val showOnAppLaunchOptionHandler: ShowOnAppLaunchOptionHandler,
4342
private val dispatcherProvider: DispatcherProvider,
44-
private val ntpAfterIdleManager: NtpAfterIdleManager,
4543
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
4644
) : BrowserLifecycleObserver {
4745

@@ -57,12 +55,10 @@ class FirstScreenHandlerImpl @Inject constructor(
5755
val lastBackgrounded = settingsDataStore.lastSessionBackgroundTimestamp
5856
val elapsed = System.currentTimeMillis() - lastBackgrounded
5957
if (lastBackgrounded == 0L || elapsed >= timeoutMs) {
60-
ntpAfterIdleManager.onNtpShownAfterIdle()
6158
showOnAppLaunchOptionHandler.handleAfterInactivityOption()
6259
return
6360
}
6461
} else if (isFreshLaunch && showOnAppLaunchFeature.self().isEnabled()) {
65-
ntpAfterIdleManager.onNtpShownUserInitiated()
6662
showOnAppLaunchOptionHandler.handleAppLaunchOption()
6763
}
6864
}

app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchOptionHandler.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.duckduckgo.appbuildconfig.api.AppBuildConfig
2828
import com.duckduckgo.common.utils.DispatcherProvider
2929
import com.duckduckgo.common.utils.isHttpOrHttps
3030
import com.duckduckgo.di.scopes.AppScope
31+
import com.duckduckgo.newtabpage.api.NtpAfterIdleManager
3132
import com.squareup.anvil.annotations.ContributesBinding
3233
import kotlinx.coroutines.flow.first
3334
import kotlinx.coroutines.withContext
@@ -50,6 +51,7 @@ class ShowOnAppLaunchOptionHandlerImpl @Inject constructor(
5051
private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore,
5152
private val tabRepository: TabRepository,
5253
private val appBuildConfig: AppBuildConfig,
54+
private val ntpAfterIdleManager: NtpAfterIdleManager,
5355
) : ShowOnAppLaunchOptionHandler {
5456

5557
override suspend fun handleAfterInactivityOption() {
@@ -60,17 +62,24 @@ class ShowOnAppLaunchOptionHandlerImpl @Inject constructor(
6062
showOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(NewTabPage)
6163
}
6264
// existing users see whatever they had selected
63-
handleAppLaunchOption()
65+
applyShowOnAppLaunchOption(fromInactivity = true)
6466
}
6567

6668
override suspend fun handleAppLaunchOption() {
69+
applyShowOnAppLaunchOption(fromInactivity = false)
70+
}
71+
72+
private suspend fun applyShowOnAppLaunchOption(fromInactivity: Boolean) {
6773
val option = showOnAppLaunchOptionDataStore.optionFlow.first()
6874
logcat { "FirstScreen: showing $option on app launch" }
6975
when (option) {
7076
LastOpenedTab -> Unit
7177
NewTabPage -> {
7278
val selectedTab = tabRepository.getSelectedTab()
7379
if (selectedTab == null || !selectedTab.url.isNullOrBlank()) {
80+
if (fromInactivity) {
81+
ntpAfterIdleManager.onIdleReturnTriggered()
82+
}
7483
tabRepository.add()
7584
}
7685
}

app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ class ShowOnAppLaunchViewModel @Inject constructor(
121121
settingsDataStore.userSelectedIdleThresholdSeconds = seconds
122122
userSelectedThreshold.value = seconds
123123
pixel.fire(SETTINGS_AFTER_INACTIVITY_TIMEOUT_CHANGED, mapOf("selectedSeconds" to seconds.toString()))
124-
ntpAfterIdleManager.fireTimeoutSelected(seconds)
124+
ntpAfterIdleManager.onIdleTimeoutSelected(seconds)
125125
}
126126
}
127127
}

app/src/test/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ import com.duckduckgo.js.messaging.api.JsCallbackData
278278
import com.duckduckgo.js.messaging.api.SubscriptionEventData
279279
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed
280280
import com.duckduckgo.malicioussiteprotection.api.MaliciousSiteProtection.Feed.MALWARE
281+
import com.duckduckgo.newtabpage.api.NtpAfterIdleManager
281282
import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels
282283
import com.duckduckgo.privacy.config.api.AmpLinkInfo
283284
import com.duckduckgo.privacy.config.api.AmpLinks
@@ -575,6 +576,7 @@ class BrowserTabViewModelTest {
575576
private val mockToggleReports: ToggleReports = mock()
576577
private val mockBrokenSitePrompt: BrokenSitePrompt = mock()
577578
private val mockTabStatsBucketing: TabStatsBucketing = mock()
579+
private val mockNtpAfterIdleManager: NtpAfterIdleManager = mock()
578580
private val mockDuckChatJSHelper: DuckChatJSHelper = mock()
579581
private val swipingTabsFeature = FakeFeatureToggleFactory.create(SwipingTabsFeature::class.java)
580582
private val swipingTabsFeatureProvider = SwipingTabsFeatureProvider(swipingTabsFeature)
@@ -925,6 +927,7 @@ class BrowserTabViewModelTest {
925927
browserUiLockFeature = fakeBrowserUiLockFeature,
926928
progressBarUpgradeFeature = fakeProgressBarUpgradeFeature,
927929
faviconFetchingFixFeature = fakeFaviconFetchingFixFeature,
930+
ntpAfterIdleManager = mockNtpAfterIdleManager,
928931
)
929932

930933
testee.loadData("abc", null, false, false)
@@ -1187,6 +1190,49 @@ class BrowserTabViewModelTest {
11871190
assertEquals("nytimes.com", omnibarViewState().omnibarText)
11881191
}
11891192

1193+
@Test
1194+
fun whenQuerySubmittedWhileOnNtpAndFeatureEnabledThenNtpSearchSubmittedNotified() {
1195+
fakeAndroidConfigBrowserFeature.showNTPAfterIdleReturn().setRawStoredState(State(enable = true))
1196+
whenever(mockOmnibarConverter.convertQueryToUrl("cats", null)).thenReturn("https://duckduckgo.com/?q=cats")
1197+
testee.globalLayoutState.value = GlobalLayoutViewState.Browser(isNewTabState = true)
1198+
1199+
testee.onUserSubmittedQuery("cats")
1200+
1201+
verify(mockNtpAfterIdleManager).onNtpSearchSubmitted()
1202+
}
1203+
1204+
@Test
1205+
fun whenQuerySubmittedWhileOnLoadedPageThenNtpSearchSubmittedNotNotified() {
1206+
fakeAndroidConfigBrowserFeature.showNTPAfterIdleReturn().setRawStoredState(State(enable = true))
1207+
whenever(mockOmnibarConverter.convertQueryToUrl("cats", null)).thenReturn("https://duckduckgo.com/?q=cats")
1208+
testee.globalLayoutState.value = GlobalLayoutViewState.Browser(isNewTabState = false)
1209+
1210+
testee.onUserSubmittedQuery("cats")
1211+
1212+
verify(mockNtpAfterIdleManager, never()).onNtpSearchSubmitted()
1213+
}
1214+
1215+
@Test
1216+
fun whenBlankQuerySubmittedWhileOnNtpThenNtpSearchSubmittedNotNotified() {
1217+
fakeAndroidConfigBrowserFeature.showNTPAfterIdleReturn().setRawStoredState(State(enable = true))
1218+
testee.globalLayoutState.value = GlobalLayoutViewState.Browser(isNewTabState = true)
1219+
1220+
testee.onUserSubmittedQuery(" ")
1221+
1222+
verify(mockNtpAfterIdleManager, never()).onNtpSearchSubmitted()
1223+
}
1224+
1225+
@Test
1226+
fun whenQuerySubmittedWhileOnNtpAndFeatureDisabledThenNtpSearchSubmittedNotNotified() {
1227+
fakeAndroidConfigBrowserFeature.showNTPAfterIdleReturn().setRawStoredState(State(enable = false))
1228+
whenever(mockOmnibarConverter.convertQueryToUrl("cats", null)).thenReturn("https://duckduckgo.com/?q=cats")
1229+
testee.globalLayoutState.value = GlobalLayoutViewState.Browser(isNewTabState = true)
1230+
1231+
testee.onUserSubmittedQuery("cats")
1232+
1233+
verify(mockNtpAfterIdleManager, never()).onNtpSearchSubmitted()
1234+
}
1235+
11901236
@Test
11911237
fun whenBrowsingAndUrlPresentThenAddBookmarkButtonEnabled() {
11921238
loadUrl("https://www.example.com", isBrowserShowing = true)

0 commit comments

Comments
 (0)