Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show on App Launch: General settings entry point #4946

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
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,11 @@
android:exported="false"
android:label="@string/generalSettingsActivityTitle"
android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" />
<activity
android:name="com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchActivity"
android:exported="false"
android:label="@string/showOnAppLaunchOptionTitle"
android:parentActivityName="com.duckduckgo.app.generalsettings.GeneralSettingsActivity" />
<activity
android:name="com.duckduckgo.app.webtrackingprotection.WebTrackingProtectionActivity"
android:exported="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,8 @@ open class BrowserActivity : DuckDuckGoActivity() {
return
}
}

viewModel.handleShowOnAppLaunchOption()
}

private fun configureObservers() {
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector
import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
import com.duckduckgo.app.fire.DataClearer
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage
import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore
import com.duckduckgo.app.global.ApplicationClearDataState
import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter
import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions
Expand Down Expand Up @@ -55,6 +60,7 @@ import com.duckduckgo.feature.toggles.api.Toggle
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber

Expand All @@ -69,6 +75,7 @@ class BrowserViewModel @Inject constructor(
private val dispatchers: DispatcherProvider,
private val pixel: Pixel,
private val skipUrlConversionOnNewTabFeature: SkipUrlConversionOnNewTabFeature,
private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore,
) : ViewModel(),
CoroutineScope {

Expand Down Expand Up @@ -284,6 +291,21 @@ class BrowserViewModel @Inject constructor(
fun onBookmarksActivityResult(url: String) {
command.value = Command.OpenSavedSite(url)
}

fun handleShowOnAppLaunchOption() {
viewModelScope.launch {
when (val option = showOnAppLaunchOptionDataStore.optionFlow.first()) {
LastOpenedTab -> Unit
NewTabPage -> onNewTabRequested()
is SpecificPage -> {
val liveSelectedTabUrl = tabRepository.getSelectedTab()?.url
if (liveSelectedTabUrl != option.url) {
onOpenInNewTabRequested(option.url)
}
}
}
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,24 @@
package com.duckduckgo.app.generalsettings

import android.os.Bundle
import android.view.View.OnClickListener
import android.widget.CompoundButton
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.browser.databinding.ActivityGeneralSettingsBinding
import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command
import com.duckduckgo.app.generalsettings.GeneralSettingsViewModel.Command.LaunchShowOnAppLaunchScreen
import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchScreenNoParams
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage
import com.duckduckgo.app.global.view.fadeTransitionConfig
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.di.scopes.ActivityScope
Expand Down Expand Up @@ -55,6 +65,10 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
viewModel.onVoiceSearchChanged(isChecked)
}

private val showOnAppLaunchClickListener = OnClickListener {
viewModel.onShowOnAppLaunchButtonClick()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand All @@ -69,7 +83,7 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
binding.autocompleteToggle.setOnCheckedChangeListener(autocompleteToggleListener)
binding.autocompleteRecentlyVisitedSitesToggle.setOnCheckedChangeListener(autocompleteRecentlyVisitedSitesToggleListener)
binding.voiceSearchToggle.setOnCheckedChangeListener(voiceSearchChangeListener)
// TODO add show on app launch setting
binding.showOnAppLaunchButton.setOnClickListener(showOnAppLaunchClickListener)
}

private fun observeViewModel() {
Expand All @@ -95,7 +109,30 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
binding.voiceSearchToggle.isVisible = true
binding.voiceSearchToggle.quietlySetIsChecked(viewState.voiceSearchEnabled, voiceSearchChangeListener)
}
setShowOnAppLaunchOptionSecondaryText(viewState.showOnAppLaunchSelectedOption)
}
}.launchIn(lifecycleScope)

viewModel.commands
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { processCommand(it) }
.launchIn(lifecycleScope)
}

private fun setShowOnAppLaunchOptionSecondaryText(showOnAppLaunchOption: ShowOnAppLaunchOption) {
val optionString = when (showOnAppLaunchOption) {
is LastOpenedTab -> getString(R.string.showOnAppLaunchOptionLastOpenedTab)
is NewTabPage -> getString(R.string.showOnAppLaunchOptionNewTabPage)
is SpecificPage -> showOnAppLaunchOption.url
}
binding.showOnAppLaunchButton.setSecondaryText(optionString)
}

private fun processCommand(command: Command) {
when (command) {
LaunchShowOnAppLaunchScreen -> {
globalActivityStarter.start(this, ShowOnAppLaunchScreenNoParams, fadeTransitionConfig())
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package com.duckduckgo.app.generalsettings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption
import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_GENERAL_SETTINGS_TOGGLED_OFF
import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_GENERAL_SETTINGS_TOGGLED_ON
import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_RECENT_SITES_GENERAL_SETTINGS_TOGGLED_OFF
Expand All @@ -33,8 +36,15 @@ import com.duckduckgo.voice.impl.VoiceSearchPixelNames.VOICE_SEARCH_GENERAL_SETT
import com.duckduckgo.voice.impl.VoiceSearchPixelNames.VOICE_SEARCH_GENERAL_SETTINGS_ON
import com.duckduckgo.voice.store.VoiceSearchRepository
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber

Expand All @@ -46,6 +56,7 @@ class GeneralSettingsViewModel @Inject constructor(
private val voiceSearchAvailability: VoiceSearchAvailability,
private val voiceSearchRepository: VoiceSearchRepository,
private val dispatcherProvider: DispatcherProvider,
private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore,
) : ViewModel() {

data class ViewState(
Expand All @@ -54,11 +65,19 @@ class GeneralSettingsViewModel @Inject constructor(
val storeHistoryEnabled: Boolean,
val showVoiceSearch: Boolean,
val voiceSearchEnabled: Boolean,
val showOnAppLaunchSelectedOption: ShowOnAppLaunchOption,
)

sealed class Command {
data object LaunchShowOnAppLaunchScreen : Command()
}

private val _viewState = MutableStateFlow<ViewState?>(null)
val viewState = _viewState.asStateFlow()

private val _commands = Channel<Command>(1, BufferOverflow.DROP_OLDEST)
val commands = _commands.receiveAsFlow()

init {
viewModelScope.launch(dispatcherProvider.io()) {
val autoCompleteEnabled = settingsDataStore.autoCompleteSuggestionsEnabled
Expand All @@ -71,8 +90,11 @@ class GeneralSettingsViewModel @Inject constructor(
storeHistoryEnabled = history.isHistoryFeatureAvailable(),
showVoiceSearch = voiceSearchAvailability.isVoiceSearchSupported,
voiceSearchEnabled = voiceSearchAvailability.isVoiceSearchAvailable,
showOnAppLaunchSelectedOption = showOnAppLaunchOptionDataStore.optionFlow.first(),
)
}

observeShowOnAppLaunchOption()
}

fun onAutocompleteSettingChanged(enabled: Boolean) {
Expand Down Expand Up @@ -119,4 +141,22 @@ class GeneralSettingsViewModel @Inject constructor(
_viewState.value = _viewState.value?.copy(voiceSearchEnabled = voiceSearchAvailability.isVoiceSearchAvailable)
}
}

fun onShowOnAppLaunchButtonClick() {
sendCommand(Command.LaunchShowOnAppLaunchScreen)
pixel.fire(AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED)
}

private fun observeShowOnAppLaunchOption() {
showOnAppLaunchOptionDataStore.optionFlow
.onEach { showOnAppLaunchOption ->
_viewState.update { it!!.copy(showOnAppLaunchSelectedOption = showOnAppLaunchOption) }
}.launchIn(viewModelScope)
}

private fun sendCommand(newCommand: Command) {
viewModelScope.launch {
_commands.send(newCommand)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.generalsettings.showonapplaunch

import android.os.Bundle
import android.view.MenuItem
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.browser.databinding.ActivityShowOnAppLaunchSettingBinding
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.viewbinding.viewBinding
import com.duckduckgo.di.scopes.ActivityScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(ShowOnAppLaunchScreenNoParams::class)
class ShowOnAppLaunchActivity : DuckDuckGoActivity() {

private val viewModel: ShowOnAppLaunchViewModel by bindViewModel()
private val binding: ActivityShowOnAppLaunchSettingBinding by viewBinding()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(binding.root)
setupToolbar(binding.includeToolbar.toolbar)

binding.specificPageUrlInput.setSelectAllOnFocus(true)

configureUiEventHandlers()
observeViewModel()
}

override fun onPause() {
super.onPause()
viewModel.setSpecificPageUrl(binding.specificPageUrlInput.text)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
true
}
else -> super.onOptionsItemSelected(item)
}
}

private fun configureUiEventHandlers() {
binding.lastOpenedTabCheckListItem.setClickListener {
viewModel.onShowOnAppLaunchOptionChanged(LastOpenedTab)
}

binding.newTabCheckListItem.setClickListener {
viewModel.onShowOnAppLaunchOptionChanged(NewTabPage)
}

binding.specificPageCheckListItem.setClickListener {
viewModel.onShowOnAppLaunchOptionChanged(SpecificPage(binding.specificPageUrlInput.text))
}

binding.specificPageUrlInput.addFocusChangedListener { _, hasFocus ->
if (hasFocus) {
viewModel.onShowOnAppLaunchOptionChanged(
SpecificPage(binding.specificPageUrlInput.text),
)
}
}
}

private fun observeViewModel() {
viewModel.viewState
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach { viewState ->
when (viewState.selectedOption) {
LastOpenedTab -> {
uncheckNewTabCheckListItem()
uncheckSpecificPageCheckListItem()
binding.lastOpenedTabCheckListItem.setChecked(true)
}
NewTabPage -> {
uncheckLastOpenedTabCheckListItem()
uncheckSpecificPageCheckListItem()
binding.newTabCheckListItem.setChecked(true)
}
is SpecificPage -> {
uncheckLastOpenedTabCheckListItem()
uncheckNewTabCheckListItem()
binding.specificPageCheckListItem.setChecked(true)
}
}

if (binding.specificPageUrlInput.text != viewState.specificPageUrl) {
binding.specificPageUrlInput.text = viewState.specificPageUrl
}
}
.launchIn(lifecycleScope)
}

private fun uncheckLastOpenedTabCheckListItem() {
binding.lastOpenedTabCheckListItem.setChecked(false)
}

private fun uncheckNewTabCheckListItem() {
binding.newTabCheckListItem.setChecked(false)
}

private fun uncheckSpecificPageCheckListItem() {
binding.specificPageCheckListItem.setChecked(false)
binding.specificPageUrlInput.isEditable = false
binding.specificPageUrlInput.isEditable = true
}
}
Loading
Loading