Skip to content

Commit

Permalink
Show on App Launch: Setting screen (#4950)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1207908166761516/1208156273709081/f

### Description

Adds the UI for the Show on App Launch settings screen and allows
switching options. Nothing is stored and no validation.

I moved RadioListItem to the `common-ui` module seeing as it’s now being
used in multiple places.


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

### Steps to test this PR

- [x] Open Settings
- [x] Open General section
- [x] Click “Show on App Launch” 
- [x] Ensure each setting can be switched
- [x] Ensure the TextInput for the “Specific Page” option is only
visible when selected
- [x] Ensure you cannot do multiple lines when entering a URL for
“Specific Page"

### UI changes


[Screen_recording_20240828_162354.webm](https://github.com/user-attachments/assets/7e45b454-2bd3-455b-af19-499ac19cabfa)

---------

Co-authored-by: Marcos Holgado <marcosh@duckduckgo.com>
Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
  • Loading branch information
3 people authored Sep 20, 2024
1 parent 03f047d commit 811fdf5
Show file tree
Hide file tree
Showing 59 changed files with 1,534 additions and 44 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 @@ -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 @@ -25,9 +25,16 @@ 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 @@ -102,7 +109,7 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
binding.voiceSearchToggle.isVisible = true
binding.voiceSearchToggle.quietlySetIsChecked(viewState.voiceSearchEnabled, voiceSearchChangeListener)
}
binding.showOnAppLaunchButton.setSecondaryText(viewState.showOnAppLaunchSelectedOptionText)
setShowOnAppLaunchOptionSecondaryText(viewState.showOnAppLaunchSelectedOption)
}
}.launchIn(lifecycleScope)

Expand All @@ -112,10 +119,19 @@ class GeneralSettingsActivity : DuckDuckGoActivity() {
.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 -> {
// TODO launch show on app launch screen
globalActivityStarter.start(this, ShowOnAppLaunchScreenNoParams, fadeTransitionConfig())
}
}
}
Expand Down
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 @@ -37,7 +40,11 @@ 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 @@ -49,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 @@ -57,7 +65,7 @@ class GeneralSettingsViewModel @Inject constructor(
val storeHistoryEnabled: Boolean,
val showVoiceSearch: Boolean,
val voiceSearchEnabled: Boolean,
val showOnAppLaunchSelectedOptionText: String,
val showOnAppLaunchSelectedOption: ShowOnAppLaunchOption,
)

sealed class Command {
Expand All @@ -82,10 +90,11 @@ class GeneralSettingsViewModel @Inject constructor(
storeHistoryEnabled = history.isHistoryFeatureAvailable(),
showVoiceSearch = voiceSearchAvailability.isVoiceSearchSupported,
voiceSearchEnabled = voiceSearchAvailability.isVoiceSearchAvailable,
// TODO get the actual value from prefs
showOnAppLaunchSelectedOptionText = "Last Opened Tab",
showOnAppLaunchSelectedOption = showOnAppLaunchOptionDataStore.optionFlow.first(),
)
}

observeShowOnAppLaunchOption()
}

fun onAutocompleteSettingChanged(enabled: Boolean) {
Expand Down Expand Up @@ -135,6 +144,14 @@ class GeneralSettingsViewModel @Inject constructor(

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) {
Expand Down
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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* 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 com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams

/**
* Use this model to launch the Show On App Launch screen
*/
object ShowOnAppLaunchScreenNoParams : ActivityParams
Loading

0 comments on commit 811fdf5

Please sign in to comment.