Skip to content

Commit

Permalink
Show on App Launch: General settings entry point (#4946)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1207908166761516/1208156273709078/f

Adds the “Show on App Launch” entry point in the General settings
screen.

We will likely not use a `String` to determine the secondary text but
this makes it easier for step by step implementation.

[Designs](https://www.figma.com/design/N2GbF5HEvopp5iwmAlMwyD/New-Tab-Page-Customization?node-id=1741-64494&t=SpS14wACEtPydb2z-4)

- [x] Open settings
- [x] Click “General”
- [x] Check “Show on App Launch” is bottom of the list

| Before  | After |
| ------ | ----- |
|
![image](https://github.com/user-attachments/assets/d0026bad-e17d-47aa-9eae-2f853669a6a9)
|
![image](https://github.com/user-attachments/assets/f031338b-9feb-4cc4-86a1-67ba9a11df58)
|

---------

Co-authored-by: Marcos Holgado <[email protected]>
Co-authored-by: Dax The Translator <[email protected]>
  • Loading branch information
3 people committed Nov 6, 2024
1 parent 5e6a526 commit b88274f
Show file tree
Hide file tree
Showing 59 changed files with 1,594 additions and 38 deletions.
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 @@ -391,6 +391,8 @@ open class BrowserActivity : DuckDuckGoActivity() {
} else {
Timber.i("shared text empty, opening last tab")
}

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 @@ -290,6 +297,21 @@ class BrowserViewModel @Inject constructor(
tabRepository.select(tabId)
}
}

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

0 comments on commit b88274f

Please sign in to comment.