Skip to content

Commit bb9abea

Browse files
authored
Show AppTP on the Input Screen (#6631)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1208671518894266/task/1211046517836482?focus=true ### Description Fixes the NTP's `hasContent` callback to also report content when only AppTP is enabled (no RMF, favorites, etc). This in turn fixes the Input Screen and makes it display AppTP banners. I also moved the logic that determined whether the Dax logo and other content should be shown on NTP into the ViewModel, so that it can be unit tested. ### Steps to test this PR - [ ] Change RMF endpoint to a JSON Blob: ```diff diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/network/RemoteMessagingService.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/network/RemoteMessagingService.kt index 7c0e619..cd65aa5 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/network/RemoteMessagingService.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/network/RemoteMessagingService.kt @@ -23,6 +23,6 @@ import retrofit2.http.GET @ContributesServiceApi(AppScope::class) interface RemoteMessagingService { - @get("https://staticcdn.duckduckgo.com/remotemessaging/config/v1/android-config.json") + @get("https://www.jsonblob.com/api/1408014913447845888") suspend fun config(): JsonRemoteMessagingConfig } ``` - [x] Install a clean build of the app. - [x] Verify that RMF (for a DuckDuckGo refresher) shows on the NTP. - [x] Go to Settings and enabled AppTP. - [x] Go back to NTP and verify that both RMF and AppTP are shown. - [x] Go to Settings -> AI Features and enabled the experimental address bar. - [x] Go back to browser and verify that both RMF and AppTP are shown when Input Screen is opened (via opening a completely new tab or clicking on the address bar). - [x] Dismiss the RMF. - [x] Verify that AppTP and Dax logo are visible on the New Tab Page. - [x] Open the Input Screen and verify that only AppTP is visible. - [x] Add a favorite. - [x] Verify that a favorite and AppTP are visible on the NTP and in the Input Screen. - [x] Go to settings and completely disable AppTP ("Disable and Delete Data" in the settings menu). - [x] Verify that only a favorite is visible on the NTP and in the Input Screen. - [x] Remove the favorite. - [x] Verify that only Dax is visible on NTP and in the Input Screen.
1 parent 6d35cd7 commit bb9abea

File tree

4 files changed

+218
-26
lines changed

4 files changed

+218
-26
lines changed

app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.Launc
4141
import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.LaunchScreen
4242
import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.SharePromoLinkRMF
4343
import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.Command.SubmitUrl
44+
import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.NewTabLegacyPageViewModelFactory
45+
import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.NewTabLegacyPageViewModelProviderFactory
4446
import com.duckduckgo.app.browser.newtab.NewTabLegacyPageViewModel.ViewState
4547
import com.duckduckgo.app.browser.remotemessage.SharePromoLinkRMFBroadCastReceiver
4648
import com.duckduckgo.app.browser.remotemessage.asMessage
@@ -53,7 +55,6 @@ import com.duckduckgo.common.ui.view.show
5355
import com.duckduckgo.common.ui.viewbinding.viewBinding
5456
import com.duckduckgo.common.utils.ConflatedJob
5557
import com.duckduckgo.common.utils.DispatcherProvider
56-
import com.duckduckgo.common.utils.ViewViewModelFactory
5758
import com.duckduckgo.di.scopes.ViewScope
5859
import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams
5960
import com.duckduckgo.navigation.api.GlobalActivityStarter
@@ -77,9 +78,6 @@ class NewTabLegacyPageView @JvmOverloads constructor(
7778
private val onHasContent: ((Boolean) -> Unit)? = null,
7879
) : LinearLayout(context, attrs, defStyle) {
7980

80-
@Inject
81-
lateinit var viewModelFactory: ViewViewModelFactory
82-
8381
@Inject
8482
lateinit var globalActivityStarter: GlobalActivityStarter
8583

@@ -102,8 +100,12 @@ class NewTabLegacyPageView @JvmOverloads constructor(
102100

103101
private val homeBackgroundLogo by lazy { HomeBackgroundLogo(binding.ddgLogo) }
104102

103+
@Inject
104+
lateinit var viewModelFactory: NewTabLegacyPageViewModelFactory
105+
105106
private val viewModel: NewTabLegacyPageViewModel by lazy {
106-
ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[NewTabLegacyPageViewModel::class.java]
107+
val providerFactory = NewTabLegacyPageViewModelProviderFactory(viewModelFactory, showDaxLogo = showLogo)
108+
ViewModelProvider(owner = findViewTreeViewModelStoreOwner()!!, factory = providerFactory)[NewTabLegacyPageViewModel::class.java]
107109
}
108110

109111
private val conflatedStateJob = ConflatedJob()
@@ -135,20 +137,12 @@ class NewTabLegacyPageView @JvmOverloads constructor(
135137
private fun render(viewState: ViewState) {
136138
logcat { "New Tab: render $viewState" }
137139

138-
val isHomeBackgroundLogoVisible = (!viewState.onboardingComplete || viewState.message == null) &&
139-
viewState.favourites.isEmpty()
140+
onHasContent?.invoke(viewState.hasContent)
140141

141-
if (!showLogo && isHomeBackgroundLogoVisible) {
142-
this.gone()
143-
onHasContent?.invoke(false)
142+
if (viewState.shouldShowLogo) {
143+
homeBackgroundLogo.showLogo()
144144
} else {
145-
this.show()
146-
onHasContent?.invoke(true)
147-
if (isHomeBackgroundLogoVisible) {
148-
homeBackgroundLogo.showLogo()
149-
} else {
150-
homeBackgroundLogo.hideLogo()
151-
}
145+
homeBackgroundLogo.hideLogo()
152146
}
153147
if (viewState.message != null && viewState.onboardingComplete) {
154148
showRemoteMessage(viewState.message, viewState.newMessage)

app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,25 @@ import android.annotation.SuppressLint
2020
import androidx.lifecycle.DefaultLifecycleObserver
2121
import androidx.lifecycle.LifecycleOwner
2222
import androidx.lifecycle.ViewModel
23+
import androidx.lifecycle.ViewModelProvider
2324
import androidx.lifecycle.viewModelScope
24-
import com.duckduckgo.anvil.annotations.ContributesViewModel
2525
import com.duckduckgo.app.browser.remotemessage.CommandActionMapper
2626
import com.duckduckgo.app.cta.db.DismissedCtaDao
2727
import com.duckduckgo.app.cta.model.CtaId
2828
import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles
2929
import com.duckduckgo.app.settings.db.SettingsDataStore
3030
import com.duckduckgo.common.utils.DispatcherProvider
3131
import com.duckduckgo.common.utils.playstore.PlayStoreUtils
32-
import com.duckduckgo.di.scopes.ViewScope
32+
import com.duckduckgo.mobile.android.app.tracking.AppTrackingProtection
3333
import com.duckduckgo.remote.messaging.api.RemoteMessage
3434
import com.duckduckgo.remote.messaging.api.RemoteMessageModel
3535
import com.duckduckgo.savedsites.api.SavedSitesRepository
3636
import com.duckduckgo.savedsites.api.models.SavedSite.Favorite
3737
import com.duckduckgo.sync.api.engine.SyncEngine
3838
import com.duckduckgo.sync.api.engine.SyncEngine.SyncTrigger.FEATURE_READ
39-
import javax.inject.Inject
39+
import dagger.assisted.Assisted
40+
import dagger.assisted.AssistedFactory
41+
import dagger.assisted.AssistedInject
4042
import kotlinx.coroutines.channels.BufferOverflow
4143
import kotlinx.coroutines.channels.Channel
4244
import kotlinx.coroutines.flow.Flow
@@ -47,12 +49,13 @@ import kotlinx.coroutines.flow.flowOn
4749
import kotlinx.coroutines.flow.launchIn
4850
import kotlinx.coroutines.flow.onEach
4951
import kotlinx.coroutines.flow.receiveAsFlow
52+
import kotlinx.coroutines.flow.update
5053
import kotlinx.coroutines.launch
5154
import kotlinx.coroutines.withContext
5255

5356
@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle
54-
@ContributesViewModel(ViewScope::class)
55-
class NewTabLegacyPageViewModel @Inject constructor(
57+
class NewTabLegacyPageViewModel @AssistedInject constructor(
58+
@Assisted private val showDaxLogo: Boolean,
5659
private val dispatchers: DispatcherProvider,
5760
private val remoteMessagingModel: RemoteMessageModel,
5861
private val playStoreUtils: PlayStoreUtils,
@@ -63,15 +66,27 @@ class NewTabLegacyPageViewModel @Inject constructor(
6366
private val extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles,
6467
private val settingsDataStore: SettingsDataStore,
6568
private val lowPriorityMessagingModel: LowPriorityMessagingModel,
69+
private val appTrackingProtection: AppTrackingProtection,
6670
) : ViewModel(), DefaultLifecycleObserver {
6771

6872
data class ViewState(
73+
private val showDaxLogo: Boolean,
74+
private val appTpEnabled: Boolean = false,
6975
val message: RemoteMessage? = null,
7076
val newMessage: Boolean = false,
7177
val onboardingComplete: Boolean = false,
7278
val favourites: List<Favorite> = emptyList(),
7379
val lowPriorityMessage: LowPriorityMessage? = null,
74-
)
80+
) {
81+
82+
private val hasContentThatDisplacesHomoLogo = onboardingComplete &&
83+
message != null ||
84+
favourites.isNotEmpty()
85+
private val hasLowPriorityMessage = lowPriorityMessage != null
86+
87+
val shouldShowLogo = !hasContentThatDisplacesHomoLogo && showDaxLogo
88+
val hasContent = shouldShowLogo || hasContentThatDisplacesHomoLogo || appTpEnabled || hasLowPriorityMessage
89+
}
7590

7691
private data class ViewStateSnapshot(
7792
val favourites: List<Favorite>,
@@ -96,7 +111,7 @@ class NewTabLegacyPageViewModel @Inject constructor(
96111
}
97112

98113
private var lastRemoteMessageSeen: RemoteMessage? = null
99-
private val _viewState = MutableStateFlow(ViewState())
114+
private val _viewState = MutableStateFlow(ViewState(showDaxLogo = showDaxLogo))
100115
val viewState = _viewState.asStateFlow()
101116

102117
private val command = Channel<Command>(1, BufferOverflow.DROP_OLDEST)
@@ -135,6 +150,12 @@ class NewTabLegacyPageViewModel @Inject constructor(
135150
.flowOn(dispatchers.main())
136151
.launchIn(viewModelScope)
137152
}
153+
154+
viewModelScope.launch {
155+
_viewState.update {
156+
it.copy(appTpEnabled = appTrackingProtection.isEnabled())
157+
}
158+
}
138159
}
139160

140161
// We only want to show New Tab when the Home CTAs from Onboarding has finished
@@ -197,4 +218,22 @@ class NewTabLegacyPageViewModel @Inject constructor(
197218
lowPriorityMessagingModel.getPrimaryButtonCommand()?.let { command.send(it) }
198219
}
199220
}
221+
222+
class NewTabLegacyPageViewModelProviderFactory(
223+
private val assistedFactory: NewTabLegacyPageViewModelFactory,
224+
private val showDaxLogo: Boolean,
225+
) : ViewModelProvider.Factory {
226+
227+
@Suppress("UNCHECKED_CAST")
228+
override fun <T : ViewModel> create(modelClass: Class<T>): T {
229+
return assistedFactory.create(showDaxLogo) as T
230+
}
231+
}
232+
233+
@AssistedFactory
234+
interface NewTabLegacyPageViewModelFactory {
235+
fun create(
236+
showDaxLogo: Boolean,
237+
): NewTabLegacyPageViewModel
238+
}
200239
}

app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModelTest.kt

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ import com.duckduckgo.common.ui.view.MessageCta
2929
import com.duckduckgo.common.utils.playstore.PlayStoreUtils
3030
import com.duckduckgo.feature.toggles.api.Toggle
3131
import com.duckduckgo.mobile.android.R
32+
import com.duckduckgo.mobile.android.app.tracking.AppTrackingProtection
3233
import com.duckduckgo.remote.messaging.api.Action
3334
import com.duckduckgo.remote.messaging.api.Content
3435
import com.duckduckgo.remote.messaging.api.RemoteMessage
3536
import com.duckduckgo.remote.messaging.api.RemoteMessageModel
3637
import com.duckduckgo.savedsites.api.SavedSitesRepository
38+
import com.duckduckgo.savedsites.api.models.SavedSite
3739
import com.duckduckgo.sync.api.engine.SyncEngine
3840
import kotlinx.coroutines.flow.flowOf
3941
import kotlinx.coroutines.test.runTest
@@ -64,17 +66,24 @@ class NewTabLegacyPageViewModelTest {
6466
private val mockExtendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles = mock()
6567
private val mockSettingsDataStore: SettingsDataStore = mock()
6668
private val mockLowPriorityMessagingModel: LowPriorityMessagingModel = mock()
69+
private val mockAppTrackingProtection: AppTrackingProtection = mock()
6770

6871
private lateinit var testee: NewTabLegacyPageViewModel
6972

7073
@Before
71-
fun setUp() {
74+
fun setUp() = runTest {
7275
val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false }
7376
whenever(mockExtendedOnboardingFeatureToggles.noBrowserCtas()).thenReturn(mockDisabledToggle)
7477
whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList()))
7578
whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(null))
79+
whenever(mockAppTrackingProtection.isEnabled()).thenReturn(false)
7680

77-
testee = NewTabLegacyPageViewModel(
81+
testee = createTestee()
82+
}
83+
84+
private fun createTestee(showLogo: Boolean = true): NewTabLegacyPageViewModel {
85+
return NewTabLegacyPageViewModel(
86+
showDaxLogo = showLogo,
7887
dispatchers = coroutinesTestRule.testDispatcherProvider,
7988
remoteMessagingModel = mockRemoteMessageModel,
8089
playStoreUtils = mockPlaystoreUtils,
@@ -85,6 +94,7 @@ class NewTabLegacyPageViewModelTest {
8594
extendedOnboardingFeatureToggles = mockExtendedOnboardingFeatureToggles,
8695
settingsDataStore = mockSettingsDataStore,
8796
lowPriorityMessagingModel = mockLowPriorityMessagingModel,
97+
appTrackingProtection = mockAppTrackingProtection,
8898
)
8999
}
90100

@@ -298,4 +308,152 @@ class NewTabLegacyPageViewModelTest {
298308
}
299309
}
300310
}
311+
312+
@Test
313+
fun `when onboarding finished and logo enabled, then show logo`() = runTest {
314+
whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true)
315+
316+
testee.onStart(mockLifecycleOwner)
317+
318+
testee.viewState.test {
319+
expectMostRecentItem().also {
320+
assertTrue(it.shouldShowLogo)
321+
assertTrue(it.hasContent)
322+
}
323+
}
324+
}
325+
326+
@Test
327+
fun `when onboarding finished and logo disabled, then hide logo and report no content`() = runTest {
328+
val testeeWithoutLogo = createTestee(showLogo = false)
329+
whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true)
330+
331+
testeeWithoutLogo.onStart(mockLifecycleOwner)
332+
333+
testeeWithoutLogo.viewState.test {
334+
expectMostRecentItem().also {
335+
assertFalse(it.shouldShowLogo)
336+
assertFalse(it.hasContent)
337+
}
338+
}
339+
}
340+
341+
@Test
342+
fun `when AppTP enabled, then show logo`() = runTest {
343+
whenever(mockAppTrackingProtection.isEnabled()).thenReturn(true)
344+
whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true)
345+
346+
testee.onStart(mockLifecycleOwner)
347+
348+
testee.viewState.test {
349+
expectMostRecentItem().also {
350+
assertTrue(it.hasContent)
351+
assertTrue(it.shouldShowLogo)
352+
}
353+
}
354+
}
355+
356+
@Test
357+
fun `when AppTP enabled and logo disabled, then hide logo`() = runTest {
358+
val testeeWithoutLogo = createTestee(showLogo = false)
359+
whenever(mockAppTrackingProtection.isEnabled()).thenReturn(true)
360+
whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true)
361+
362+
testeeWithoutLogo.onStart(mockLifecycleOwner)
363+
364+
testeeWithoutLogo.viewState.test {
365+
expectMostRecentItem().also {
366+
assertFalse(it.shouldShowLogo)
367+
assertTrue(it.hasContent)
368+
}
369+
}
370+
}
371+
372+
@Test
373+
fun `when onboarding complete and RMF available, then hide logo`() = runTest {
374+
val remoteMessage = RemoteMessage("id1", Content.Small("", ""), emptyList(), emptyList())
375+
whenever(mockRemoteMessageModel.getActiveMessages()).thenReturn(flowOf(remoteMessage))
376+
whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true)
377+
378+
testee.onStart(mockLifecycleOwner)
379+
380+
testee.viewState.test {
381+
expectMostRecentItem().also {
382+
assertFalse(it.shouldShowLogo)
383+
assertTrue(it.hasContent)
384+
}
385+
}
386+
}
387+
388+
@Test
389+
fun `when favorites available, then hide logo`() = runTest {
390+
val favorites = listOf(
391+
SavedSite.Favorite("1", "Test", "https://test.com", lastModified = "2024-01-01", 0),
392+
)
393+
whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(favorites))
394+
whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true)
395+
whenever(mockAppTrackingProtection.isEnabled()).thenReturn(false)
396+
397+
testee.onStart(mockLifecycleOwner)
398+
399+
testee.viewState.test {
400+
expectMostRecentItem().also {
401+
assertFalse(it.shouldShowLogo)
402+
assertTrue(it.hasContent)
403+
}
404+
}
405+
}
406+
407+
@Test
408+
fun `when low priority message available, then show logo`() = runTest {
409+
val lowPriorityMessage = LowPriorityMessage.DefaultBrowserMessage(
410+
message = MessageCta.Message(
411+
topIllustration = R.drawable.ic_device_mobile_default,
412+
title = "Set as default browser",
413+
action = "Set as default",
414+
action2 = "Do not ask again",
415+
),
416+
onPrimaryAction = {},
417+
onSecondaryAction = {},
418+
onClose = {},
419+
onShown = {},
420+
)
421+
whenever(mockLowPriorityMessagingModel.getMessage()).thenReturn(lowPriorityMessage)
422+
423+
testee.onStart(mockLifecycleOwner)
424+
425+
testee.viewState.test {
426+
expectMostRecentItem().also {
427+
assertTrue(it.shouldShowLogo)
428+
assertTrue(it.hasContent)
429+
}
430+
}
431+
}
432+
433+
@Test
434+
fun `when low priority message available and logo disabled, then hide logo`() = runTest {
435+
val testeeWithoutLogo = createTestee(showLogo = false)
436+
val lowPriorityMessage = LowPriorityMessage.DefaultBrowserMessage(
437+
message = MessageCta.Message(
438+
topIllustration = R.drawable.ic_device_mobile_default,
439+
title = "Set as default browser",
440+
action = "Set as default",
441+
action2 = "Do not ask again",
442+
),
443+
onPrimaryAction = {},
444+
onSecondaryAction = {},
445+
onClose = {},
446+
onShown = {},
447+
)
448+
whenever(mockLowPriorityMessagingModel.getMessage()).thenReturn(lowPriorityMessage)
449+
450+
testeeWithoutLogo.onStart(mockLifecycleOwner)
451+
452+
testeeWithoutLogo.viewState.test {
453+
expectMostRecentItem().also {
454+
assertFalse(it.shouldShowLogo)
455+
assertTrue(it.hasContent)
456+
}
457+
}
458+
}
301459
}

duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/tabs/SearchTabFragment.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class SearchTabFragment : DuckDuckGoFragment(R.layout.fragment_search_tab) {
104104
newTabPagePlugins.getPlugins().firstOrNull()?.let { plugin ->
105105
val newTabPageView = plugin.getView(requireContext(), showLogo = false) { hasContent ->
106106
viewModel.onNewTabPageContentChanged(hasContent)
107+
binding.newTabContainerLayout.isVisible = hasContent
107108
}
108109
binding.newTabContainerLayout.addView(newTabPageView)
109110
}

0 commit comments

Comments
 (0)