Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions app/lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4124,17 +4124,6 @@
column="39"/>
</issue>

<issue
id="DenyListedApi"
message="Use com.duckduckgo.data.store.api.SharedPreferencesProvider instead"
errorLine1=" private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) }"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt"
line="32"
column="58"/>
</issue>

<issue
id="DenyListedApi"
message="Use com.duckduckgo.data.store.api.SharedPreferencesProvider instead"
Expand Down
70 changes: 50 additions & 20 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ import com.duckduckgo.app.global.view.ClearDataAction
import com.duckduckgo.app.global.view.ORIGIN_DUCK_AI_CONTEXTUAL_CHAT
import com.duckduckgo.app.global.view.ORIGIN_HATCH
import com.duckduckgo.app.global.view.renderIfChanged
import com.duckduckgo.app.onboarding.orchestrator.NewUserBrowserOnboardingViewModel
import com.duckduckgo.app.onboarding.ui.OnboardingActivity
import com.duckduckgo.app.onboarding.ui.page.DefaultBrowserPage
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.pixels.AppPixelName.FIRE_DIALOG_CANCEL
Expand Down Expand Up @@ -275,6 +277,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
}

private val viewModel: BrowserViewModel by bindViewModel()
private val onboardingHostViewModel: NewUserBrowserOnboardingViewModel by bindViewModel()
private val duckChatViewModel: DuckChatSharedViewModel by viewModels()

private var instanceStateBundles: CombinedInstanceState? = null
Expand Down Expand Up @@ -429,6 +432,7 @@ open class BrowserActivity : DuckDuckGoActivity() {
.launchIn(lifecycleScope)

observeDuckChatSharedCommands()
observeOnboardingHost()
observeBrowserModeChanges()

viewModel.awaitClearDataFinishedNotification()
Expand Down Expand Up @@ -528,20 +532,23 @@ open class BrowserActivity : DuckDuckGoActivity() {
if (pendingDuckAiOnboardingFire) {
pendingDuckAiOnboardingFire = false
closeDuckChatFullScreen()
onboardingHostViewModel.onDuckAiFireCompleted()
}
}
FireDialog.EVENT_ON_SINGLE_TAB_CLEAR_FEATURE_NOT_SUPPORTED -> {
showSnackbar(R.string.singleTabFireDialogClearNotSupportedSnackbar)
if (pendingDuckAiOnboardingFire) {
pendingDuckAiOnboardingFire = false
closeDuckChatFullScreen()
onboardingHostViewModel.onDuckAiFireCompleted()
}
}
FireDialog.EVENT_ON_SINGLE_TAB_CLEAR_ERROR -> {
showSnackbar(R.string.singleTabFireDialogClearErrorSnackbar)
if (pendingDuckAiOnboardingFire) {
pendingDuckAiOnboardingFire = false
closeDuckChatFullScreen()
onboardingHostViewModel.onDuckAiFireCompleted()
Comment thread
LukasPaczos marked this conversation as resolved.
}
}
}
Expand Down Expand Up @@ -851,26 +858,9 @@ open class BrowserActivity : DuckDuckGoActivity() {
}

if (intent.getBooleanExtra(OPEN_DUCK_CHAT, false)) {
isDuckChatVisible = true
if (duckAiFeatureState.showInputScreenAutomaticallyOnNewTab.value) {
externalIntentProcessingState.onIntentRequestToOpenDuckAi()
}

if (duckAiFeatureState.showFullScreenMode.value) {
val url = intent.getStringExtra(DUCK_CHAT_URL) ?: duckChat.getDuckChatUrl("", false)
// The tab to return to when this Duck.ai tab is closed.
// Use to the current tab if no explicit tab id is passed.
// Falls back to NTP otherwise.
val sourceTabId = intent.getStringExtra(SOURCE_TAB_ID_EXTRA) ?: currentTab?.tabId
if (swipingTabsFeature.isEnabled) {
launchNewTab(query = url, skipHome = false, sourceTabId = sourceTabId)
} else {
lifecycleScope.launch { viewModel.onOpenInNewTabRequested(query = url, sourceTabId = sourceTabId, skipHome = false) }
}
} else {
val duckChatSessionActive = intent.getBooleanExtra(DUCK_CHAT_SESSION_ACTIVE, false)
viewModel.openDuckChat(intent.getStringExtra(DUCK_CHAT_URL), duckChatSessionActive, withTransition = duckAiShouldAnimate)
}
val duckChatSessionActive = intent.getBooleanExtra(DUCK_CHAT_SESSION_ACTIVE, false)
val sourceTabId = intent.getStringExtra(SOURCE_TAB_ID_EXTRA)
launchDuckAi(url = intent.getStringExtra(DUCK_CHAT_URL), duckChatSessionActive = duckChatSessionActive, sourceTabId = sourceTabId)
return
}

Expand Down Expand Up @@ -1101,6 +1091,28 @@ open class BrowserActivity : DuckDuckGoActivity() {
globalActivityStarter.start(this, DownloadsScreenNoParams)
}

private fun launchDuckAi(url: String?, duckChatSessionActive: Boolean = false, sourceTabId: String? = null) {
isDuckChatVisible = true
if (duckAiFeatureState.showInputScreenAutomaticallyOnNewTab.value) {
externalIntentProcessingState.onIntentRequestToOpenDuckAi()
}

if (duckAiFeatureState.showFullScreenMode.value) {
// The tab to return to when this Duck.ai tab is closed.
// Use to the current tab if no explicit tab id is passed.
// Falls back to NTP otherwise.
val sourceTabId = sourceTabId ?: currentTab?.tabId
val fullScreenUrl = url ?: duckChat.getDuckChatUrl("", false)
if (swipingTabsFeature.isEnabled) {
launchNewTab(query = fullScreenUrl, skipHome = false, sourceTabId = sourceTabId)
} else {
lifecycleScope.launch { viewModel.onOpenInNewTabRequested(query = fullScreenUrl, sourceTabId = sourceTabId, skipHome = false) }
}
} else {
viewModel.openDuckChat(url, duckChatSessionActive, withTransition = duckAiShouldAnimate)
}
}

fun closeDuckChatFullScreen() {
isDuckChatVisible = false
externalIntentProcessingState.onDuckAiClosed()
Expand Down Expand Up @@ -1297,6 +1309,24 @@ open class BrowserActivity : DuckDuckGoActivity() {
}
}

private fun observeOnboardingHost() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
onboardingHostViewModel.commands.collect { command ->
when (command) {
NewUserBrowserOnboardingViewModel.Command.HandOffToOnboardingActivity -> {
startActivity(OnboardingActivity.intent(this@BrowserActivity))
finish()
}
is NewUserBrowserOnboardingViewModel.Command.OpenDuckAiOnboardingDemo -> {
launchDuckAi(url = command.url)
}
}
}
}
}
}

/**
* Recreates the activity whenever the user switches browser mode. The activity is built for
* one mode at a time. The previous mode's tab state is stripped from the saved bundle, and any
Expand Down
21 changes: 18 additions & 3 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ import com.duckduckgo.app.global.model.orderedTrackerBlockedEntities
import com.duckduckgo.app.global.view.NonDismissibleBehavior
import com.duckduckgo.app.global.view.launchDefaultAppActivity
import com.duckduckgo.app.global.view.renderIfChanged
import com.duckduckgo.app.onboarding.store.OnboardingStore
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature
import com.duckduckgo.app.settings.db.SettingsDataStore
Expand Down Expand Up @@ -331,6 +332,7 @@ import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload
import com.duckduckgo.duckchat.api.DuckAiFeatureState
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.api.DuckChatHistoryNoParams
import com.duckduckgo.duckchat.api.InputMode
import com.duckduckgo.duckchat.api.inputscreen.BrowserAndInputScreenTransitionProvider
import com.duckduckgo.duckchat.api.inputscreen.DuckAiOnboardingEndCtaVariant
import com.duckduckgo.duckchat.api.inputscreen.InputScreenActivityParams
Expand Down Expand Up @@ -694,6 +696,9 @@ class BrowserTabFragment :
@Inject
lateinit var edgeToEdgeHandler: EdgeToEdgeHandler

@Inject
lateinit var onboardingStore: OnboardingStore

/**
* We use this to monitor whether the user was seeing the in-context Email Protection signup prompt
* This is needed because the activity stack will be cleared if an external link is opened in our browser
Expand Down Expand Up @@ -1390,7 +1395,7 @@ class BrowserTabFragment :
private fun configureInputScreenLauncher() {
omnibar.configureInputScreenLaunchListener { query ->
if (!nativeInputManager.isNativeInputEnabled()) {
launchInputScreen(query, isNewTab = omnibar.viewMode == NewTab)
launchInputScreen(query, isNewTab = omnibar.viewMode == NewTab, launchOnChat = omnibar.viewMode == ViewMode.DuckAI)
} else {
removeDuckChatContextualSheet()
showNativeInput(query)
Expand All @@ -1406,6 +1411,11 @@ class BrowserTabFragment :
tabs = viewModel.tabs,
currentTabUrl = viewModel.siteLiveData.asFlow().map { it?.url },
query = query,
initialInputMode = if (onboardingStore.consumeOpenInputOnDuckAiTab()) {
InputMode.DUCK_AI
} else {
null
},
callbacks = NativeInputCallbacks(
onSearchTextChanged = { text -> onUserEnteredText(text) },
onClearAutocomplete = {
Expand Down Expand Up @@ -1504,6 +1514,7 @@ class BrowserTabFragment :
query: String,
isNewTab: Boolean = false,
duckAiEndCtaVariant: DuckAiOnboardingEndCtaVariant = DuckAiOnboardingEndCtaVariant.NONE,
launchOnChat: Boolean = false,
) {
logcat { "Duck.ai: launchInputScreen" }
val isTopOmnibar = omnibar.omnibarType != OmnibarType.SINGLE_BOTTOM
Expand All @@ -1514,7 +1525,11 @@ class BrowserTabFragment :
query = query,
isTopOmnibar = isTopOmnibar,
browserButtonsConfig = InputScreenBrowserButtonsConfig.Enabled(tabs = viewModel.tabs.value?.size ?: 0),
launchOnChat = omnibar.viewMode == ViewMode.DuckAI,
initialInputMode = if (launchOnChat) {
InputMode.DUCK_AI
} else {
null
},
isNewTab = isNewTab,
showReturnHatch = androidBrowserConfigFeature.showNTPAfterIdleReturn().isEnabled(),
duckAiOnboardingEndCta = duckAiEndCtaVariant,
Expand Down Expand Up @@ -3023,7 +3038,7 @@ class BrowserTabFragment :
is Command.LaunchInputScreen -> {
// if the fire button is used, prevent automatically launching the input screen until the process reloads
if ((requireActivity() as? BrowserActivity)?.isDataClearingInProgress == false) {
launchInputScreen(query = "", isNewTab = true, duckAiEndCtaVariant = it.duckAiEndCtaVariant)
launchInputScreen(query = "", isNewTab = true, duckAiEndCtaVariant = it.duckAiEndCtaVariant, launchOnChat = it.launchOnChat)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,8 @@ class BrowserTabViewModel @Inject constructor(
val hasPendingOnboardingPromo = ctaViewModel.isPromoOnboardingDialogShowing()
if (!hasPendingOnboardingPromo) {
val duckAiEndCtaVariant = ctaViewModel.prepareAndMarkDuckAiEndCtaForInputScreen()
command.value = LaunchInputScreen(duckAiEndCtaVariant = duckAiEndCtaVariant)
val launchOnChat = onboardingStore.consumeOpenInputOnDuckAiTab()
command.value = LaunchInputScreen(duckAiEndCtaVariant = duckAiEndCtaVariant, launchOnChat = launchOnChat)
}
}
}
Expand Down Expand Up @@ -5595,6 +5596,11 @@ class BrowserTabViewModel @Inject constructor(
}

fun dismissDuckAiFireOnboardingCta() {
// Custom AI onboarding defers dismissal to the end of the linear orchestrator
// run, so the CTA survives if the app is killed mid-onboarding and the flow
// has to re-run on next launch.
if (onboardingStore.isCustomAiOnboardingFlow()) return

val cta = ctaViewState.value?.cta ?: return
if (cta is OnboardingDaxDialogCta.DaxDuckAiFireButtonCta || cta is DaxDuckAiFireButtonBrandDesignUpdateContextualCta) {
viewModelScope.launch { ctaViewModel.onUserDismissedCta(cta = cta) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ sealed class Command {

data class LaunchInputScreen(
val duckAiEndCtaVariant: DuckAiOnboardingEndCtaVariant = DuckAiOnboardingEndCtaVariant.NONE,
val launchOnChat: Boolean = false,
) : Command()

data object LaunchDuckChatHistory : Command()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import com.duckduckgo.common.ui.view.toPx
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.duckchat.api.DuckAiFeatureState
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.api.InputMode
import com.duckduckgo.duckchat.api.toChatIdOrNull
import com.duckduckgo.duckchat.impl.ui.nativeinput.views.NativeInputWidget
import com.duckduckgo.navigation.api.GlobalActivityStarter
Expand Down Expand Up @@ -114,7 +115,9 @@ interface NativeInputManager {
currentTabUrl: Flow<String?>,
query: String = "",
callbacks: NativeInputCallbacks,
initialInputMode: InputMode? = null,
)

fun hideNativeInput(animate: Boolean = true, isNavigation: Boolean = false): Boolean
fun handleDuckAiVoiceResult(query: String)
fun onKeyboardVisibilityChanged(isVisible: Boolean)
Expand Down Expand Up @@ -363,6 +366,7 @@ class RealNativeInputManager @Inject constructor(
currentTabUrl: Flow<String?>,
query: String,
callbacks: NativeInputCallbacks,
initialInputMode: InputMode?,
) {
if (!isNativeInputFieldEnabled) return

Expand Down Expand Up @@ -408,7 +412,7 @@ class RealNativeInputManager @Inject constructor(
}
attachWidget(widgetView, isBottom, tabId)
val isNewTab = query.isEmpty() && omnibarController.getText().isEmpty()
applyInitialTabSelection(widgetView, isNewTab)
applyInitialTabSelection(widgetView, isNewTab, initialInputMode)
if (omnibarController.isDuckAiMode()) {
widgetFrom(widgetView)?.setToggleVisible(false)
} else {
Expand Down Expand Up @@ -640,12 +644,16 @@ class RealNativeInputManager @Inject constructor(
}
}

private fun applyInitialTabSelection(widgetView: View, isNewTab: Boolean) {
private fun applyInitialTabSelection(widgetView: View, isNewTab: Boolean, initialInputMode: InputMode?) {
val widget = widgetFrom(widgetView) ?: return
if (omnibarController.isDuckAiMode()) {
widget.selectChatTab()
} else if (isNewTab) {
widget.applyDefaultTogglePosition()
when (initialInputMode) {
InputMode.DUCK_AI -> widget.selectChatTab()
InputMode.SEARCH -> widget.selectSearchTab()
null -> if (omnibarController.isDuckAiMode()) {
widget.selectChatTab()
} else if (isNewTab) {
widget.applyDefaultTogglePosition()
}
}
}

Expand Down
20 changes: 19 additions & 1 deletion app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ import com.duckduckgo.app.cta.model.CtaId
import com.duckduckgo.app.cta.model.DismissedCta
import com.duckduckgo.app.cta.ui.HomePanelCta.AddWidgetAutoOnboarding
import com.duckduckgo.app.cta.ui.HomePanelCta.AddWidgetInstructions
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.global.install.AppInstallStore
import com.duckduckgo.app.global.install.daysInstalled
import com.duckduckgo.app.global.model.Site
import com.duckduckgo.app.global.model.domain
import com.duckduckgo.app.global.model.orderedTrackerBlockedEntities
import com.duckduckgo.app.onboarding.DuckAiOnboardingExperimentMetrics
import com.duckduckgo.app.onboarding.orchestrator.NewUserOnboardingPlanProvider
import com.duckduckgo.app.onboarding.store.AppStage
import com.duckduckgo.app.onboarding.store.OnboardingStore
import com.duckduckgo.app.onboarding.store.UserStageStore
Expand All @@ -55,18 +57,24 @@ import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.duckchat.api.DuckChat
import com.duckduckgo.duckchat.api.inputscreen.DuckAiOnboardingEndCtaVariant
import com.duckduckgo.onboarding.api.LinearOnboardingOrchestrator
import com.duckduckgo.onboarding.api.LinearOnboardingState
import com.duckduckgo.onboarding.api.forPlan
import com.duckduckgo.subscriptions.api.SubscriptionPromoCtaShownPlugin
import com.duckduckgo.subscriptions.api.SubscriptionStatus
import com.duckduckgo.subscriptions.api.Subscriptions
import dagger.SingleInstanceIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -97,6 +105,8 @@ class CtaViewModel @Inject constructor(
private val appTheme: AppTheme,
private val duckAiOnboardingExperimentMetrics: DuckAiOnboardingExperimentMetrics,
private val deviceInfo: DeviceInfo,
@AppCoroutineScope private val coroutineScope: CoroutineScope,
linearOnboardingOrchestrator: LinearOnboardingOrchestrator,
) {
@ExperimentalCoroutinesApi
@VisibleForTesting
Expand All @@ -113,6 +123,14 @@ class CtaViewModel @Inject constructor(
}
}

init {
linearOnboardingOrchestrator.state
.forPlan(NewUserOnboardingPlanProvider.ROOT_PLAN_ID)
.filterIsInstance<LinearOnboardingState.Completed>()
.onEach { completeStageIfDaxOnboardingCompleted() }
.launchIn(coroutineScope)
}

private suspend fun isSubscriptionCtaAvailable(): Boolean =
subscriptions.isEligible() && hasNoSubscription() && extendedOnboardingFeatureToggles.privacyProCta().isEnabled()

Expand Down Expand Up @@ -232,7 +250,7 @@ class CtaViewModel @Inject constructor(

suspend fun prepareAndMarkDuckAiEndCtaForInputScreen(): DuckAiOnboardingEndCtaVariant {
return withContext(dispatchers.io()) {
val shouldShow = canShowDuckAiEndCta() && !extendedOnboardingFeatureToggles.noBrowserCtas().isEnabled()
val shouldShow = canShowDuckAiEndCta() && !extendedOnboardingFeatureToggles.noBrowserCtas().isEnabled() && !settingsDataStore.hideTips
if (!shouldShow) return@withContext DuckAiOnboardingEndCtaVariant.NONE

dismissedCtaDao.insert(DismissedCta(CtaId.DAX_DUCK_AI_END))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,4 @@ interface CustomDuckAiOnboardingFeature {

@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
fun self(): Toggle

@Toggle.DefaultValue(DefaultFeatureValue.FALSE)
fun introAnimation(): Toggle
}
Loading
Loading