diff --git a/app/src/internal/AndroidManifest.xml b/app/src/internal/AndroidManifest.xml
index b89dff89ff6f..82091edb2c82 100644
--- a/app/src/internal/AndroidManifest.xml
+++ b/app/src/internal/AndroidManifest.xml
@@ -7,6 +7,10 @@
android:name="com.duckduckgo.app.dev.settings.DevSettingsActivity"
android:label="@string/devSettingsTitle"
android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" />
+
sendTdsIntent()
- is Command.OpenUASelector -> showUASelector()
- is Command.ShowSavedSitesClearedConfirmation -> showSavedSitesClearedConfirmation()
- is Command.ChangePrivacyConfigUrl -> showChangePrivacyUrl()
- is Command.CustomTabs -> showCustomTabs()
- else -> TODO()
+ is SendTdsIntent -> sendTdsIntent()
+ is OpenUASelector -> showUASelector()
+ is ShowSavedSitesClearedConfirmation -> showSavedSitesClearedConfirmation()
+ is ChangePrivacyConfigUrl -> showChangePrivacyUrl()
+ is CustomTabs -> showCustomTabs()
+ Notifications -> showNotifications()
}
}
@@ -167,6 +175,10 @@ class DevSettingsActivity : DuckDuckGoActivity() {
startActivity(CustomTabsInternalSettingsActivity.intent(this), options)
}
+ private fun showNotifications() {
+ startActivity(NotificationsActivity.intent(this))
+ }
+
companion object {
fun intent(context: Context): Intent {
return Intent(context, DevSettingsActivity::class.java)
diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt
index e4328b2ca6a3..bb08f10b8076 100644
--- a/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt
+++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/DevSettingsViewModel.kt
@@ -60,6 +60,7 @@ class DevSettingsViewModel @Inject constructor(
object ShowSavedSitesClearedConfirmation : Command()
object ChangePrivacyConfigUrl : Command()
object CustomTabs : Command()
+ data object Notifications : Command()
}
private val viewState = MutableStateFlow(ViewState())
@@ -137,4 +138,8 @@ class DevSettingsViewModel @Inject constructor(
command.send(Command.ShowSavedSitesClearedConfirmation)
}
}
+
+ fun notificationsClicked() {
+ viewModelScope.launch { command.send(Command.Notifications) }
+ }
}
diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsActivity.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsActivity.kt
new file mode 100644
index 000000000000..04e8e32f8f8b
--- /dev/null
+++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsActivity.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.dev.settings.notifications
+
+import android.app.Notification
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.core.app.NotificationManagerCompat
+import androidx.lifecycle.Lifecycle.State.STARTED
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.duckduckgo.anvil.annotations.InjectWith
+import com.duckduckgo.app.browser.databinding.ActivityNotificationsBinding
+import com.duckduckgo.app.dev.settings.notifications.NotificationViewModel.Command.TriggerNotification
+import com.duckduckgo.app.dev.settings.notifications.NotificationViewModel.ViewState
+import com.duckduckgo.app.notification.NotificationFactory
+import com.duckduckgo.common.ui.DuckDuckGoActivity
+import com.duckduckgo.common.ui.view.listitem.TwoLineListItem
+import com.duckduckgo.common.ui.viewbinding.viewBinding
+import com.duckduckgo.common.utils.notification.checkPermissionAndNotify
+import com.duckduckgo.di.scopes.ActivityScope
+import javax.inject.Inject
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+
+@InjectWith(ActivityScope::class)
+class NotificationsActivity : DuckDuckGoActivity() {
+
+ @Inject
+ lateinit var viewModel: NotificationViewModel
+
+ @Inject
+ lateinit var factory: NotificationFactory
+
+ private val binding: ActivityNotificationsBinding by viewBinding()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(binding.root)
+ setupToolbar(binding.includeToolbar.toolbar)
+
+ observeViewState()
+ observeCommands()
+ }
+
+ private fun observeViewState() {
+ viewModel.viewState.flowWithLifecycle(lifecycle, STARTED).onEach { render(it) }
+ .launchIn(lifecycleScope)
+ }
+
+ private fun observeCommands() {
+ viewModel.command.flowWithLifecycle(lifecycle, STARTED).onEach { command ->
+ when (command) {
+ is TriggerNotification -> addNotification(id = command.notificationItem.id, notification = command.notificationItem.notification)
+ }
+ }.launchIn(lifecycleScope)
+ }
+
+ private fun render(viewState: ViewState) {
+ viewState.scheduledNotifications.forEach { notificationItem ->
+ buildNotificationItem(
+ title = notificationItem.title,
+ subtitle = notificationItem.subtitle,
+ onClick = { viewModel.onNotificationItemClick(notificationItem) },
+ ).also {
+ binding.scheduledNotificationsContainer.addView(it)
+ }
+ }
+
+ viewState.vpnNotifications.forEach { notificationItem ->
+ buildNotificationItem(
+ title = notificationItem.title,
+ subtitle = notificationItem.subtitle,
+ onClick = { viewModel.onNotificationItemClick(notificationItem) },
+ ).also {
+ binding.vpnNotificationsContainer.addView(it)
+ }
+ }
+ }
+
+ private fun buildNotificationItem(
+ title: String,
+ subtitle: String,
+ onClick: () -> Unit,
+ ): TwoLineListItem {
+ return TwoLineListItem(this).apply {
+ setPrimaryText(title)
+ setSecondaryText(subtitle)
+ setOnClickListener { onClick() }
+ }
+ }
+
+ private fun addNotification(
+ id: Int,
+ notification: Notification,
+ ) {
+ NotificationManagerCompat.from(this)
+ .checkPermissionAndNotify(context = this, id = id, notification = notification)
+ }
+
+ companion object {
+
+ fun intent(context: Context): Intent {
+ return Intent(context, NotificationsActivity::class.java)
+ }
+ }
+}
diff --git a/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsViewModel.kt b/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsViewModel.kt
new file mode 100644
index 000000000000..ea7fa2e67062
--- /dev/null
+++ b/app/src/internal/java/com/duckduckgo/app/dev/settings/notifications/NotificationsViewModel.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.dev.settings.notifications
+
+import android.app.Notification
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.duckduckgo.anvil.annotations.ContributesViewModel
+import com.duckduckgo.app.dev.settings.notifications.NotificationViewModel.ViewState.NotificationItem
+import com.duckduckgo.app.notification.NotificationFactory
+import com.duckduckgo.app.notification.model.SchedulableNotificationPlugin
+import com.duckduckgo.app.survey.api.SurveyRepository
+import com.duckduckgo.app.survey.model.Survey
+import com.duckduckgo.app.survey.model.Survey.Status.SCHEDULED
+import com.duckduckgo.app.survey.notification.SurveyAvailableNotification
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.common.utils.plugins.PluginPoint
+import com.duckduckgo.di.scopes.ActivityScope
+import com.duckduckgo.networkprotection.impl.notification.NetPDisabledNotificationBuilder
+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.receiveAsFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@ContributesViewModel(ActivityScope::class)
+class NotificationViewModel @Inject constructor(
+ private val applicationContext: Context,
+ private val dispatcher: DispatcherProvider,
+ private val schedulableNotificationPluginPoint: PluginPoint,
+ private val factory: NotificationFactory,
+ private val surveyRepository: SurveyRepository,
+ private val netPDisabledNotificationBuilder: NetPDisabledNotificationBuilder,
+) : ViewModel() {
+
+ data class ViewState(
+ val scheduledNotifications: List = emptyList(),
+ val vpnNotifications: List = emptyList(),
+ ) {
+
+ data class NotificationItem(
+ val id: Int,
+ val title: String,
+ val subtitle: String,
+ val notification: Notification,
+ )
+ }
+
+ sealed class Command {
+ data class TriggerNotification(val notificationItem: NotificationItem) : Command()
+ }
+
+ private val _viewState = MutableStateFlow(ViewState())
+ val viewState = _viewState.asStateFlow()
+
+ private val _command = Channel(1, BufferOverflow.DROP_OLDEST)
+ val command = _command.receiveAsFlow()
+
+ init {
+ viewModelScope.launch {
+ val scheduledNotificationItems = schedulableNotificationPluginPoint.getPlugins().map { plugin ->
+
+ // The survey notification will crash if we do not have a survey in the database
+ if (plugin.getSchedulableNotification().javaClass == SurveyAvailableNotification::class.java) {
+ withContext(dispatcher.io()) {
+ addTestSurvey()
+ }
+ }
+
+ // the survey intent hits the DB, so we need to do this on IO
+ val launchIntent = withContext(dispatcher.io()) { plugin.getLaunchIntent() }
+
+ NotificationItem(
+ id = plugin.getSpecification().systemId,
+ title = plugin.getSpecification().title,
+ subtitle = plugin.getSpecification().description,
+ notification = factory.createNotification(plugin.getSpecification(), launchIntent, null),
+ )
+ }
+
+ val netPDisabledNotificationItem = NotificationItem(
+ id = 0,
+ title = "NetP Disabled",
+ subtitle = "NetP is disabled",
+ notification = netPDisabledNotificationBuilder.buildVpnAccessRevokedNotification(applicationContext),
+ )
+
+ _viewState.update {
+ it.copy(
+ scheduledNotifications = scheduledNotificationItems,
+ vpnNotifications = listOf(netPDisabledNotificationItem),
+ )
+ }
+ }
+ }
+
+ private fun addTestSurvey() {
+ surveyRepository.persistSurvey(
+ Survey(
+ "testSurveyId",
+ "https://youtu.be/dQw4w9WgXcQ?si=iztopgFbXoWUnoOE",
+ daysInstalled = 1,
+ status = SCHEDULED,
+ ),
+ )
+ }
+
+ fun onNotificationItemClick(notificationItem: NotificationItem) {
+ viewModelScope.launch {
+ _command.send(Command.TriggerNotification(notificationItem))
+ }
+ }
+}
diff --git a/app/src/internal/res/layout/activity_dev_settings.xml b/app/src/internal/res/layout/activity_dev_settings.xml
index bc48624014ef..52027f50670b 100644
--- a/app/src/internal/res/layout/activity_dev_settings.xml
+++ b/app/src/internal/res/layout/activity_dev_settings.xml
@@ -88,6 +88,13 @@
app:secondaryText="@string/devSettingsScreenCustomTabsSubtitle"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/internal/res/values/donottranslate.xml b/app/src/internal/res/values/donottranslate.xml
index 88938dcba1e3..8f0c14a1338a 100644
--- a/app/src/internal/res/values/donottranslate.xml
+++ b/app/src/internal/res/values/donottranslate.xml
@@ -38,6 +38,8 @@
Override UserAgent
Override Privacy Remote Config URL
Custom Tabs
+ Notifications
+ Trigger notifications for testing
Click here to customize the Privacy Remote Config URL
Load a Custom Tab for a specified URL
UserAgent
@@ -87,4 +89,8 @@
Enter a URL
Default Browser
+
+ Scheduled Notifications
+ VPN Notifications
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 534d53d49c85..86b74a6e7283 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -325,6 +325,24 @@
+
+
+
+
+
+
+
+
+
+
+
+ @Inject
+ lateinit var addWidgetLauncher: AddWidgetLauncher
+
+ @Inject
+ lateinit var appBuildConfig: AppBuildConfig
+
+ @Inject
+ lateinit var globalActivityStarter: GlobalActivityStarter
+
+ @Inject
+ lateinit var _proSettingsPlugin: PluginPoint
+ private val proSettingsPlugin by lazy {
+ _proSettingsPlugin.getPlugins()
+ }
+
+ @Inject
+ lateinit var _duckPlayerSettingsPlugin: PluginPoint
+ private val duckPlayerSettingsPlugin by lazy {
+ _duckPlayerSettingsPlugin.getPlugins()
+ }
+
+ @Inject
+ lateinit var newSettingsFeature: NewSettingsFeature
+
+ private val viewsPrivacy
+ get() = binding.includeSettings.contentSettingsPrivacy
+
+ private val viewsSettings
+ get() = binding.includeSettings.contentSettingsSettings
+
+ private val viewsMore
+ get() = binding.includeSettings.contentSettingsMore
+
+ private val viewsInternal
+ get() = binding.includeSettings.contentSettingsInternal
+
+ private val viewsPro
+ get() = binding.includeSettings.settingsSectionPro
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(binding.root)
+ setupToolbar(binding.includeToolbar.toolbar)
+
+ configureUiEventHandlers()
+ configureInternalFeatures()
+ configureSettings()
+ lifecycle.addObserver(viewModel)
+ observeViewModel()
+
+ intent?.getStringExtra(BrowserActivity.LAUNCH_FROM_NOTIFICATION_PIXEL_NAME)?.let {
+ viewModel.onLaunchedFromNotification(it)
+ }
+ }
+
+ private fun configureUiEventHandlers() {
+ with(viewsPrivacy) {
+ setAsDefaultBrowserSetting.setClickListener { viewModel.onDefaultBrowserSettingClicked() }
+ privateSearchSetting.setClickListener { viewModel.onPrivateSearchSettingClicked() }
+ webTrackingProtectionSetting.setClickListener { viewModel.onWebTrackingProtectionSettingClicked() }
+ cookiePopupProtectionSetting.setClickListener { viewModel.onCookiePopupProtectionSettingClicked() }
+ emailSetting.setClickListener { viewModel.onEmailProtectionSettingClicked() }
+ vpnSetting.setClickListener { viewModel.onAppTPSettingClicked() }
+ }
+
+ with(viewsSettings) {
+ homeScreenWidgetSetting.setClickListener { viewModel.userRequestedToAddHomeScreenWidget() }
+ autofillLoginsSetting.setClickListener { viewModel.onAutofillSettingsClick() }
+ syncSetting.setClickListener { viewModel.onSyncSettingClicked() }
+ fireButtonSetting.setClickListener { viewModel.onFireButtonSettingClicked() }
+ permissionsSetting.setClickListener { viewModel.onPermissionsSettingClicked() }
+ appearanceSetting.setClickListener { viewModel.onAppearanceSettingClicked() }
+ accessibilitySetting.setClickListener { viewModel.onAccessibilitySettingClicked() }
+ aboutSetting.setClickListener { viewModel.onAboutSettingClicked() }
+ generalSetting.setClickListener { viewModel.onGeneralSettingClicked() }
+ }
+
+ with(viewsMore) {
+ macOsSetting.setClickListener { viewModel.onMacOsSettingClicked() }
+ windowsSetting.setClickListener { viewModel.windowsSettingClicked() }
+ }
+ }
+
+ private fun configureSettings() {
+ if (proSettingsPlugin.isEmpty()) {
+ viewsPro.gone()
+ } else {
+ proSettingsPlugin.forEach { plugin ->
+ viewsPro.addView(plugin.getView(this))
+ }
+ }
+
+ if (duckPlayerSettingsPlugin.isEmpty()) {
+ viewsSettings.settingsSectionDuckPlayer.gone()
+ } else {
+ duckPlayerSettingsPlugin.forEach { plugin ->
+ viewsSettings.settingsSectionDuckPlayer.addView(plugin.getView(this))
+ }
+ }
+ }
+
+ private fun configureInternalFeatures() {
+ viewsInternal.settingsSectionInternal.visibility = if (internalFeaturePlugins.getPlugins().isEmpty()) View.GONE else View.VISIBLE
+ internalFeaturePlugins.getPlugins().forEach { feature ->
+ Timber.v("Adding internal feature ${feature.internalFeatureTitle()}")
+ val view = TwoLineListItem(this).apply {
+ setPrimaryText(feature.internalFeatureTitle())
+ setSecondaryText(feature.internalFeatureSubtitle())
+ }
+ viewsInternal.settingsInternalFeaturesContainer.addView(view)
+ view.setClickListener { feature.onInternalFeatureClicked(this) }
+ }
+ }
+
+ private fun observeViewModel() {
+ viewModel.viewState()
+ .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
+ .distinctUntilChanged()
+ .onEach { viewState ->
+ viewState.let {
+ updateDefaultBrowserViewVisibility(it)
+ updateDeviceShieldSettings(
+ it.appTrackingProtectionEnabled,
+ it.appTrackingProtectionOnboardingShown,
+ )
+ updateEmailSubtitle(it.emailAddress)
+ updateAutofill(it.showAutofill)
+ updateSyncSetting(visible = it.showSyncSetting)
+ updateAutoconsent(it.isAutoconsentEnabled)
+ updatePrivacyPro(it.isPrivacyProEnabled)
+ updateDuckPlayer(it.isDuckPlayerEnabled)
+ }
+ }.launchIn(lifecycleScope)
+
+ viewModel.commands()
+ .flowWithLifecycle(lifecycle, Lifecycle.State.CREATED)
+ .onEach { processCommand(it) }
+ .launchIn(lifecycleScope)
+ }
+
+ private fun updatePrivacyPro(isPrivacyProEnabled: Boolean) {
+ if (isPrivacyProEnabled) {
+ pixel.fire(PRIVACY_PRO_IS_ENABLED_AND_ELIGIBLE, type = Daily())
+ viewsPro.show()
+ } else {
+ viewsPro.gone()
+ }
+ }
+
+ private fun updateDuckPlayer(isDuckPlayerEnabled: Boolean) {
+ if (isDuckPlayerEnabled) {
+ viewsSettings.settingsSectionDuckPlayer.show()
+ } else {
+ viewsSettings.settingsSectionDuckPlayer.gone()
+ }
+ }
+
+ private fun updateAutofill(autofillEnabled: Boolean) = with(viewsSettings.autofillLoginsSetting) {
+ visibility = if (autofillEnabled) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+
+ private fun updateEmailSubtitle(emailAddress: String?) {
+ if (emailAddress.isNullOrEmpty()) {
+ viewsPrivacy.emailSetting.setSecondaryText(getString(R.string.settingsEmailProtectionSubtitle))
+ viewsPrivacy.emailSetting.setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
+ } else {
+ viewsPrivacy.emailSetting.setSecondaryText(emailAddress)
+ viewsPrivacy.emailSetting.setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
+ }
+ }
+
+ private fun updateSyncSetting(visible: Boolean) {
+ with(viewsSettings.syncSetting) {
+ isVisible = visible
+ }
+ }
+
+ private fun updateAutoconsent(enabled: Boolean) {
+ if (enabled) {
+ viewsPrivacy.cookiePopupProtectionSetting.setSecondaryText(getString(R.string.cookiePopupProtectionEnabled))
+ viewsPrivacy.cookiePopupProtectionSetting.setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
+ } else {
+ viewsPrivacy.cookiePopupProtectionSetting.setSecondaryText(getString(R.string.cookiePopupProtectionDescription))
+ viewsPrivacy.cookiePopupProtectionSetting.setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
+ }
+ }
+
+ private fun processCommand(it: Command?) {
+ when (it) {
+ is Command.LaunchDefaultBrowser -> launchDefaultAppScreen()
+ is Command.LaunchAutofillSettings -> launchAutofillSettings()
+ is Command.LaunchAccessibilitySettings -> launchAccessibilitySettings()
+ is Command.LaunchAppTPTrackersScreen -> launchAppTPTrackersScreen()
+ is Command.LaunchAppTPOnboarding -> launchAppTPOnboardingScreen()
+ is Command.LaunchEmailProtection -> launchEmailProtectionScreen(it.url)
+ is Command.LaunchEmailProtectionNotSupported -> launchEmailProtectionNotSupported()
+ is Command.LaunchAddHomeScreenWidget -> launchAddHomeScreenWidget()
+ is Command.LaunchMacOs -> launchMacOsScreen()
+ is Command.LaunchWindows -> launchWindowsScreen()
+ is Command.LaunchSyncSettings -> launchSyncSettings()
+ is Command.LaunchPrivateSearchWebPage -> launchPrivateSearchScreen()
+ is Command.LaunchWebTrackingProtectionScreen -> launchWebTrackingProtectionScreen()
+ is Command.LaunchCookiePopupProtectionScreen -> launchCookiePopupProtectionScreen()
+ is Command.LaunchFireButtonScreen -> launchFireButtonScreen()
+ is Command.LaunchPermissionsScreen -> launchPermissionsScreen()
+ is Command.LaunchAppearanceScreen -> launchAppearanceScreen()
+ is Command.LaunchAboutScreen -> launchAboutScreen()
+ is Command.LaunchGeneralSettingsScreen -> launchGeneralSettingsScreen()
+ null -> TODO()
+ }
+ }
+
+ private fun updateDefaultBrowserViewVisibility(it: LegacySettingsViewModel.ViewState) {
+ with(viewsPrivacy.setAsDefaultBrowserSetting) {
+ visibility = if (it.showDefaultBrowserSetting) {
+ if (it.isAppDefaultBrowser) {
+ setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
+ setSecondaryText(getString(R.string.settingsDefaultBrowserSetDescription))
+ } else {
+ setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
+ setSecondaryText(getString(R.string.settingsDefaultBrowserNotSetDescription))
+ }
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+ }
+
+ private fun updateDeviceShieldSettings(
+ appTPEnabled: Boolean,
+ appTrackingProtectionOnboardingShown: Boolean,
+ ) {
+ with(viewsPrivacy) {
+ if (appTPEnabled) {
+ vpnSetting.setSecondaryText(getString(R.string.atp_SettingsDeviceShieldEnabled))
+ vpnSetting.setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
+ } else {
+ if (appTrackingProtectionOnboardingShown) {
+ vpnSetting.setSecondaryText(getString(R.string.atp_SettingsDeviceShieldDisabled))
+ vpnSetting.setItemStatus(CheckListItem.CheckItemStatus.WARNING)
+ } else {
+ vpnSetting.setSecondaryText(getString(R.string.atp_SettingsDeviceShieldNeverEnabled))
+ vpnSetting.setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
+ }
+ }
+ }
+ }
+
+ private fun launchDefaultAppScreen() {
+ launchDefaultAppActivity()
+ }
+
+ private fun launchAutofillSettings() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AutofillSettingsScreen(source = AutofillSettingsLaunchSource.SettingsActivity), options)
+ }
+
+ private fun launchAccessibilitySettings() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AccessibilityScreens.Default, options)
+ }
+
+ private fun launchEmailProtectionScreen(url: String) {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ startActivity(BrowserActivity.intent(this, url, interstitialScreen = true), options)
+ this.finish()
+ }
+
+ private fun launchEmailProtectionNotSupported() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, EmailProtectionUnsupportedScreenNoParams, options)
+ }
+
+ private fun launchMacOsScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, MacOsScreenWithEmptyParams, options)
+ }
+
+ private fun launchWindowsScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, WindowsScreenWithEmptyParams, options)
+ }
+
+ private fun launchSyncSettings() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, SyncActivityWithEmptyParams, options)
+ }
+
+ private fun launchAppTPTrackersScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AppTrackerActivityWithEmptyParams, options)
+ }
+
+ private fun launchAppTPOnboardingScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AppTrackerOnboardingActivityWithEmptyParamsParams, options)
+ }
+
+ private fun launchAddHomeScreenWidget() {
+ pixel.fire(AppPixelName.SETTINGS_ADD_HOME_SCREEN_WIDGET_CLICKED)
+ addWidgetLauncher.launchAddWidget(this)
+ }
+
+ private fun launchPrivateSearchScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, PrivateSearchScreenNoParams, options)
+ }
+
+ private fun launchWebTrackingProtectionScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, WebTrackingProtectionScreenNoParams, options)
+ }
+
+ private fun launchCookiePopupProtectionScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ startActivity(AutoconsentSettingsActivity.intent(this), options)
+ }
+
+ private fun launchFireButtonScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, FireButtonScreenNoParams, options)
+ }
+
+ private fun launchPermissionsScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, PermissionsScreenNoParams, options)
+ }
+
+ private fun launchAppearanceScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AppearanceScreen.Default, options)
+ }
+
+ private fun launchAboutScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AboutScreenNoParams, options)
+ }
+
+ private fun launchGeneralSettingsScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, GeneralSettingsScreenNoParams, options)
+ }
+
+ companion object {
+ const val LAUNCH_FROM_NOTIFICATION_PIXEL_NAME = "LAUNCH_FROM_NOTIFICATION_PIXEL_NAME"
+
+ fun intent(context: Context): Intent {
+ return Intent(context, LegacySettingsActivity::class.java)
+ }
+ }
+}
diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/LegacySettingsViewModel.kt
similarity index 98%
rename from app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt
rename to app/src/main/java/com/duckduckgo/app/settings/LegacySettingsViewModel.kt
index 22abfba72720..18a6184f2814 100644
--- a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt
+++ b/app/src/main/java/com/duckduckgo/app/settings/LegacySettingsViewModel.kt
@@ -51,7 +51,7 @@ import kotlinx.coroutines.launch
@SuppressLint("NoLifecycleObserver")
@ContributesViewModel(ActivityScope::class)
-class SettingsViewModel @Inject constructor(
+class LegacySettingsViewModel @Inject constructor(
private val defaultWebBrowserCapability: DefaultBrowserDetector,
private val appTrackingProtection: AppTrackingProtection,
private val pixel: Pixel,
@@ -222,7 +222,7 @@ class SettingsViewModel @Inject constructor(
} else {
Command.LaunchEmailProtectionNotSupported
}
- this@SettingsViewModel.command.send(command)
+ this@LegacySettingsViewModel.command.send(command)
}
pixel.fire(SETTINGS_EMAIL_PROTECTION_PRESSED)
}
diff --git a/app/src/main/java/com/duckduckgo/app/settings/NewSettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/NewSettingsActivity.kt
new file mode 100644
index 000000000000..b88de6a32bf8
--- /dev/null
+++ b/app/src/main/java/com/duckduckgo/app/settings/NewSettingsActivity.kt
@@ -0,0 +1,441 @@
+/*
+ * 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.settings
+
+import android.app.ActivityOptions
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.View
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
+import com.duckduckgo.anvil.annotations.InjectWith
+import com.duckduckgo.app.about.AboutScreenNoParams
+import com.duckduckgo.app.accessibility.AccessibilityScreens
+import com.duckduckgo.app.appearance.AppearanceScreen
+import com.duckduckgo.app.browser.BrowserActivity
+import com.duckduckgo.app.browser.databinding.ActivitySettingsNewBinding
+import com.duckduckgo.app.email.ui.EmailProtectionUnsupportedScreenNoParams
+import com.duckduckgo.app.firebutton.FireButtonScreenNoParams
+import com.duckduckgo.app.generalsettings.GeneralSettingsScreenNoParams
+import com.duckduckgo.app.global.view.launchDefaultAppActivity
+import com.duckduckgo.app.permissions.PermissionsScreenNoParams
+import com.duckduckgo.app.pixels.AppPixelName
+import com.duckduckgo.app.pixels.AppPixelName.PRIVACY_PRO_IS_ENABLED_AND_ELIGIBLE
+import com.duckduckgo.app.privatesearch.PrivateSearchScreenNoParams
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command
+import com.duckduckgo.app.statistics.pixels.Pixel
+import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
+import com.duckduckgo.app.webtrackingprotection.WebTrackingProtectionScreenNoParams
+import com.duckduckgo.app.widget.AddWidgetLauncher
+import com.duckduckgo.appbuildconfig.api.AppBuildConfig
+import com.duckduckgo.autoconsent.impl.ui.AutoconsentSettingsActivity
+import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreen
+import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource
+import com.duckduckgo.common.ui.DuckDuckGoActivity
+import com.duckduckgo.common.ui.view.gone
+import com.duckduckgo.common.ui.view.listitem.CheckListItem
+import com.duckduckgo.common.ui.view.listitem.TwoLineListItem
+import com.duckduckgo.common.ui.view.show
+import com.duckduckgo.common.ui.viewbinding.viewBinding
+import com.duckduckgo.common.utils.plugins.PluginPoint
+import com.duckduckgo.di.scopes.ActivityScope
+import com.duckduckgo.internal.features.api.InternalFeaturePlugin
+import com.duckduckgo.macos.api.MacOsScreenWithEmptyParams
+import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerActivityWithEmptyParams
+import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams
+import com.duckduckgo.navigation.api.GlobalActivityStarter
+import com.duckduckgo.settings.api.DuckPlayerSettingsPlugin
+import com.duckduckgo.settings.api.ProSettingsPlugin
+import com.duckduckgo.sync.api.SyncActivityWithEmptyParams
+import com.duckduckgo.windows.api.ui.WindowsScreenWithEmptyParams
+import javax.inject.Inject
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import timber.log.Timber
+
+@InjectWith(ActivityScope::class)
+class NewSettingsActivity : DuckDuckGoActivity() {
+
+ private val viewModel: NewSettingsViewModel by bindViewModel()
+ private val binding: ActivitySettingsNewBinding by viewBinding()
+
+ @Inject
+ lateinit var pixel: Pixel
+
+ @Inject
+ lateinit var internalFeaturePlugins: PluginPoint
+
+ @Inject
+ lateinit var addWidgetLauncher: AddWidgetLauncher
+
+ @Inject
+ lateinit var appBuildConfig: AppBuildConfig
+
+ @Inject
+ lateinit var globalActivityStarter: GlobalActivityStarter
+
+ @Inject
+ lateinit var _proSettingsPlugin: PluginPoint
+ private val proSettingsPlugin by lazy {
+ _proSettingsPlugin.getPlugins()
+ }
+
+ @Inject
+ lateinit var _duckPlayerSettingsPlugin: PluginPoint
+ private val duckPlayerSettingsPlugin by lazy {
+ _duckPlayerSettingsPlugin.getPlugins()
+ }
+
+ private val viewsPrivacy
+ get() = binding.includeSettings.contentSettingsPrivacy
+
+ private val viewsSettings
+ get() = binding.includeSettings.contentSettingsSettings
+
+ private val viewsMore
+ get() = binding.includeSettings.contentSettingsMore
+
+ private val viewsInternal
+ get() = binding.includeSettings.contentSettingsInternal
+
+ private val viewsPro
+ get() = binding.includeSettings.settingsSectionPro
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(binding.root)
+ setupToolbar(binding.includeToolbar.toolbar)
+
+ configureUiEventHandlers()
+ configureInternalFeatures()
+ configureSettings()
+ lifecycle.addObserver(viewModel)
+ observeViewModel()
+
+ intent?.getStringExtra(BrowserActivity.LAUNCH_FROM_NOTIFICATION_PIXEL_NAME)?.let {
+ viewModel.onLaunchedFromNotification(it)
+ }
+ }
+
+ private fun configureUiEventHandlers() {
+ with(viewsPrivacy) {
+ setAsDefaultBrowserSetting.setClickListener { viewModel.onDefaultBrowserSettingClicked() }
+ privateSearchSetting.setClickListener { viewModel.onPrivateSearchSettingClicked() }
+ webTrackingProtectionSetting.setClickListener { viewModel.onWebTrackingProtectionSettingClicked() }
+ cookiePopupProtectionSetting.setClickListener { viewModel.onCookiePopupProtectionSettingClicked() }
+ emailSetting.setClickListener { viewModel.onEmailProtectionSettingClicked() }
+ vpnSetting.setClickListener { viewModel.onAppTPSettingClicked() }
+ }
+
+ with(viewsSettings) {
+ homeScreenWidgetSetting.setClickListener { viewModel.userRequestedToAddHomeScreenWidget() }
+ autofillLoginsSetting.setClickListener { viewModel.onAutofillSettingsClick() }
+ syncSetting.setClickListener { viewModel.onSyncSettingClicked() }
+ fireButtonSetting.setClickListener { viewModel.onFireButtonSettingClicked() }
+ permissionsSetting.setClickListener { viewModel.onPermissionsSettingClicked() }
+ appearanceSetting.setClickListener { viewModel.onAppearanceSettingClicked() }
+ accessibilitySetting.setClickListener { viewModel.onAccessibilitySettingClicked() }
+ aboutSetting.setClickListener { viewModel.onAboutSettingClicked() }
+ generalSetting.setClickListener { viewModel.onGeneralSettingClicked() }
+ }
+
+ with(viewsMore) {
+ macOsSetting.setClickListener { viewModel.onMacOsSettingClicked() }
+ windowsSetting.setClickListener { viewModel.windowsSettingClicked() }
+ }
+ }
+
+ private fun configureSettings() {
+ if (proSettingsPlugin.isEmpty()) {
+ viewsPro.gone()
+ } else {
+ proSettingsPlugin.forEach { plugin ->
+ viewsPro.addView(plugin.getView(this))
+ }
+ }
+
+ if (duckPlayerSettingsPlugin.isEmpty()) {
+ viewsSettings.settingsSectionDuckPlayer.gone()
+ } else {
+ duckPlayerSettingsPlugin.forEach { plugin ->
+ viewsSettings.settingsSectionDuckPlayer.addView(plugin.getView(this))
+ }
+ }
+ }
+
+ private fun configureInternalFeatures() {
+ viewsInternal.settingsSectionInternal.visibility = if (internalFeaturePlugins.getPlugins().isEmpty()) View.GONE else View.VISIBLE
+ internalFeaturePlugins.getPlugins().forEach { feature ->
+ Timber.v("Adding internal feature ${feature.internalFeatureTitle()}")
+ val view = TwoLineListItem(this).apply {
+ setPrimaryText(feature.internalFeatureTitle())
+ setSecondaryText(feature.internalFeatureSubtitle())
+ }
+ viewsInternal.settingsInternalFeaturesContainer.addView(view)
+ view.setClickListener { feature.onInternalFeatureClicked(this) }
+ }
+ }
+
+ private fun observeViewModel() {
+ viewModel.viewState()
+ .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
+ .distinctUntilChanged()
+ .onEach { viewState ->
+ viewState.let {
+ updateDefaultBrowserViewVisibility(it)
+ updateDeviceShieldSettings(
+ it.appTrackingProtectionEnabled,
+ it.appTrackingProtectionOnboardingShown,
+ )
+ updateEmailSubtitle(it.emailAddress)
+ updateAutofill(it.showAutofill)
+ updateSyncSetting(visible = it.showSyncSetting)
+ updateAutoconsent(it.isAutoconsentEnabled)
+ updatePrivacyPro(it.isPrivacyProEnabled)
+ updateDuckPlayer(it.isDuckPlayerEnabled)
+ }
+ }.launchIn(lifecycleScope)
+
+ viewModel.commands()
+ .flowWithLifecycle(lifecycle, Lifecycle.State.CREATED)
+ .onEach { processCommand(it) }
+ .launchIn(lifecycleScope)
+ }
+
+ private fun updatePrivacyPro(isPrivacyProEnabled: Boolean) {
+ if (isPrivacyProEnabled) {
+ pixel.fire(PRIVACY_PRO_IS_ENABLED_AND_ELIGIBLE, type = Daily())
+ viewsPro.show()
+ } else {
+ viewsPro.gone()
+ }
+ }
+
+ private fun updateDuckPlayer(isDuckPlayerEnabled: Boolean) {
+ if (isDuckPlayerEnabled) {
+ viewsSettings.settingsSectionDuckPlayer.show()
+ } else {
+ viewsSettings.settingsSectionDuckPlayer.gone()
+ }
+ }
+
+ private fun updateAutofill(autofillEnabled: Boolean) = with(viewsSettings.autofillLoginsSetting) {
+ visibility = if (autofillEnabled) {
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+
+ private fun updateEmailSubtitle(emailAddress: String?) {
+ if (emailAddress.isNullOrEmpty()) {
+ viewsPrivacy.emailSetting.setSecondaryText(getString(com.duckduckgo.app.browser.R.string.settingsEmailProtectionSubtitle))
+ viewsPrivacy.emailSetting.setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
+ } else {
+ viewsPrivacy.emailSetting.setSecondaryText(emailAddress)
+ viewsPrivacy.emailSetting.setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
+ }
+ }
+
+ private fun updateSyncSetting(visible: Boolean) {
+ with(viewsSettings.syncSetting) {
+ isVisible = visible
+ }
+ }
+
+ private fun updateAutoconsent(enabled: Boolean) {
+ if (enabled) {
+ viewsPrivacy.cookiePopupProtectionSetting.setSecondaryText(getString(com.duckduckgo.app.browser.R.string.cookiePopupProtectionEnabled))
+ viewsPrivacy.cookiePopupProtectionSetting.setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
+ } else {
+ viewsPrivacy.cookiePopupProtectionSetting.setSecondaryText(
+ getString(com.duckduckgo.app.browser.R.string.cookiePopupProtectionDescription),
+ )
+ viewsPrivacy.cookiePopupProtectionSetting.setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
+ }
+ }
+
+ private fun processCommand(it: Command?) {
+ when (it) {
+ is Command.LaunchDefaultBrowser -> launchDefaultAppScreen()
+ is Command.LaunchAutofillSettings -> launchAutofillSettings()
+ is Command.LaunchAccessibilitySettings -> launchAccessibilitySettings()
+ is Command.LaunchAppTPTrackersScreen -> launchAppTPTrackersScreen()
+ is Command.LaunchAppTPOnboarding -> launchAppTPOnboardingScreen()
+ is Command.LaunchEmailProtection -> launchEmailProtectionScreen(it.url)
+ is Command.LaunchEmailProtectionNotSupported -> launchEmailProtectionNotSupported()
+ is Command.LaunchAddHomeScreenWidget -> launchAddHomeScreenWidget()
+ is Command.LaunchMacOs -> launchMacOsScreen()
+ is Command.LaunchWindows -> launchWindowsScreen()
+ is Command.LaunchSyncSettings -> launchSyncSettings()
+ is Command.LaunchPrivateSearchWebPage -> launchPrivateSearchScreen()
+ is Command.LaunchWebTrackingProtectionScreen -> launchWebTrackingProtectionScreen()
+ is Command.LaunchCookiePopupProtectionScreen -> launchCookiePopupProtectionScreen()
+ is Command.LaunchFireButtonScreen -> launchFireButtonScreen()
+ is Command.LaunchPermissionsScreen -> launchPermissionsScreen()
+ is Command.LaunchAppearanceScreen -> launchAppearanceScreen()
+ is Command.LaunchAboutScreen -> launchAboutScreen()
+ is Command.LaunchGeneralSettingsScreen -> launchGeneralSettingsScreen()
+ null -> TODO()
+ }
+ }
+
+ private fun updateDefaultBrowserViewVisibility(it: NewSettingsViewModel.ViewState) {
+ with(viewsPrivacy.setAsDefaultBrowserSetting) {
+ visibility = if (it.showDefaultBrowserSetting) {
+ if (it.isAppDefaultBrowser) {
+ setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
+ setSecondaryText(getString(com.duckduckgo.app.browser.R.string.settingsDefaultBrowserSetDescription))
+ } else {
+ setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
+ setSecondaryText(getString(com.duckduckgo.app.browser.R.string.settingsDefaultBrowserNotSetDescription))
+ }
+ View.VISIBLE
+ } else {
+ View.GONE
+ }
+ }
+ }
+
+ private fun updateDeviceShieldSettings(
+ appTPEnabled: Boolean,
+ appTrackingProtectionOnboardingShown: Boolean,
+ ) {
+ with(viewsPrivacy) {
+ if (appTPEnabled) {
+ vpnSetting.setSecondaryText(getString(com.duckduckgo.app.browser.R.string.atp_SettingsDeviceShieldEnabled))
+ vpnSetting.setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
+ } else {
+ if (appTrackingProtectionOnboardingShown) {
+ vpnSetting.setSecondaryText(getString(com.duckduckgo.app.browser.R.string.atp_SettingsDeviceShieldDisabled))
+ vpnSetting.setItemStatus(CheckListItem.CheckItemStatus.WARNING)
+ } else {
+ vpnSetting.setSecondaryText(getString(com.duckduckgo.app.browser.R.string.atp_SettingsDeviceShieldNeverEnabled))
+ vpnSetting.setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
+ }
+ }
+ }
+ }
+
+ private fun launchDefaultAppScreen() {
+ launchDefaultAppActivity()
+ }
+
+ private fun launchAutofillSettings() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AutofillSettingsScreen(source = AutofillSettingsLaunchSource.SettingsActivity), options)
+ }
+
+ private fun launchAccessibilitySettings() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AccessibilityScreens.Default, options)
+ }
+
+ private fun launchEmailProtectionScreen(url: String) {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ startActivity(BrowserActivity.intent(this, url, interstitialScreen = true), options)
+ this.finish()
+ }
+
+ private fun launchEmailProtectionNotSupported() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, EmailProtectionUnsupportedScreenNoParams, options)
+ }
+
+ private fun launchMacOsScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, MacOsScreenWithEmptyParams, options)
+ }
+
+ private fun launchWindowsScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, WindowsScreenWithEmptyParams, options)
+ }
+
+ private fun launchSyncSettings() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, SyncActivityWithEmptyParams, options)
+ }
+
+ private fun launchAppTPTrackersScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AppTrackerActivityWithEmptyParams, options)
+ }
+
+ private fun launchAppTPOnboardingScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AppTrackerOnboardingActivityWithEmptyParamsParams, options)
+ }
+
+ private fun launchAddHomeScreenWidget() {
+ pixel.fire(AppPixelName.SETTINGS_ADD_HOME_SCREEN_WIDGET_CLICKED)
+ addWidgetLauncher.launchAddWidget(this)
+ }
+
+ private fun launchPrivateSearchScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, PrivateSearchScreenNoParams, options)
+ }
+
+ private fun launchWebTrackingProtectionScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, WebTrackingProtectionScreenNoParams, options)
+ }
+
+ private fun launchCookiePopupProtectionScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ startActivity(AutoconsentSettingsActivity.intent(this), options)
+ }
+
+ private fun launchFireButtonScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, FireButtonScreenNoParams, options)
+ }
+
+ private fun launchPermissionsScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, PermissionsScreenNoParams, options)
+ }
+
+ private fun launchAppearanceScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AppearanceScreen.Default, options)
+ }
+
+ private fun launchAboutScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, AboutScreenNoParams, options)
+ }
+
+ private fun launchGeneralSettingsScreen() {
+ val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
+ globalActivityStarter.start(this, GeneralSettingsScreenNoParams, options)
+ }
+
+ companion object {
+ const val LAUNCH_FROM_NOTIFICATION_PIXEL_NAME = "LAUNCH_FROM_NOTIFICATION_PIXEL_NAME"
+
+ fun intent(context: Context): Intent {
+ return Intent(context, NewSettingsActivity::class.java)
+ }
+ }
+}
diff --git a/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt
new file mode 100644
index 000000000000..cbe93507e5f1
--- /dev/null
+++ b/app/src/main/java/com/duckduckgo/app/settings/NewSettingsViewModel.kt
@@ -0,0 +1,303 @@
+/*
+ * 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.settings
+
+import android.annotation.SuppressLint
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.duckduckgo.anvil.annotations.ContributesViewModel
+import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector
+import com.duckduckgo.app.pixels.AppPixelName.*
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAboutScreen
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAccessibilitySettings
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAddHomeScreenWidget
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAppTPOnboarding
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAppTPTrackersScreen
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAppearanceScreen
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchAutofillSettings
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchCookiePopupProtectionScreen
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchDefaultBrowser
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchEmailProtection
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchEmailProtectionNotSupported
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchFireButtonScreen
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchGeneralSettingsScreen
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchMacOs
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchPermissionsScreen
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchPrivateSearchWebPage
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchSyncSettings
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchWebTrackingProtectionScreen
+import com.duckduckgo.app.settings.NewSettingsViewModel.Command.LaunchWindows
+import com.duckduckgo.app.statistics.pixels.Pixel
+import com.duckduckgo.autoconsent.api.Autoconsent
+import com.duckduckgo.autofill.api.AutofillCapabilityChecker
+import com.duckduckgo.autofill.api.email.EmailManager
+import com.duckduckgo.common.utils.ConflatedJob
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.di.scopes.ActivityScope
+import com.duckduckgo.duckplayer.api.DuckPlayer
+import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED_WIH_HELP_LINK
+import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED
+import com.duckduckgo.mobile.android.app.tracking.AppTrackingProtection
+import com.duckduckgo.subscriptions.api.Subscriptions
+import com.duckduckgo.sync.api.DeviceSyncState
+import javax.inject.Inject
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.receiveAsFlow
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+
+@SuppressLint("NoLifecycleObserver")
+@ContributesViewModel(ActivityScope::class)
+class NewSettingsViewModel @Inject constructor(
+ private val defaultWebBrowserCapability: DefaultBrowserDetector,
+ private val appTrackingProtection: AppTrackingProtection,
+ private val pixel: Pixel,
+ private val emailManager: EmailManager,
+ private val autofillCapabilityChecker: AutofillCapabilityChecker,
+ private val deviceSyncState: DeviceSyncState,
+ private val dispatcherProvider: DispatcherProvider,
+ private val autoconsent: Autoconsent,
+ private val subscriptions: Subscriptions,
+ private val duckPlayer: DuckPlayer,
+) : ViewModel(), DefaultLifecycleObserver {
+
+ data class ViewState(
+ val showDefaultBrowserSetting: Boolean = false,
+ val isAppDefaultBrowser: Boolean = false,
+ val appTrackingProtectionOnboardingShown: Boolean = false,
+ val appTrackingProtectionEnabled: Boolean = false,
+ val emailAddress: String? = null,
+ val showAutofill: Boolean = false,
+ val showSyncSetting: Boolean = false,
+ val isAutoconsentEnabled: Boolean = false,
+ val isPrivacyProEnabled: Boolean = false,
+ val isDuckPlayerEnabled: Boolean = false,
+ )
+
+ sealed class Command {
+ data object LaunchDefaultBrowser : Command()
+ data class LaunchEmailProtection(val url: String) : Command()
+ data object LaunchEmailProtectionNotSupported : Command()
+ data object LaunchAutofillSettings : Command()
+ data object LaunchAccessibilitySettings : Command()
+ data object LaunchAddHomeScreenWidget : Command()
+ data object LaunchAppTPTrackersScreen : Command()
+ data object LaunchAppTPOnboarding : Command()
+ data object LaunchMacOs : Command()
+ data object LaunchWindows : Command()
+ data object LaunchSyncSettings : Command()
+ data object LaunchPrivateSearchWebPage : Command()
+ data object LaunchWebTrackingProtectionScreen : Command()
+ data object LaunchCookiePopupProtectionScreen : Command()
+ data object LaunchFireButtonScreen : Command()
+ data object LaunchPermissionsScreen : Command()
+ data object LaunchAppearanceScreen : Command()
+ data object LaunchAboutScreen : Command()
+ data object LaunchGeneralSettingsScreen : Command()
+ }
+
+ private val viewState = MutableStateFlow(ViewState())
+
+ private val command = Channel(1, BufferOverflow.DROP_OLDEST)
+ private val appTPPollJob = ConflatedJob()
+
+ init {
+ pixel.fire(SETTINGS_OPENED)
+ }
+
+ override fun onStart(owner: LifecycleOwner) {
+ super.onStart(owner)
+ start()
+ startPollingAppTPState()
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ super.onStop(owner)
+ appTPPollJob.cancel()
+ }
+
+ @VisibleForTesting
+ internal fun start() {
+ val defaultBrowserAlready = defaultWebBrowserCapability.isDefaultBrowser()
+
+ viewModelScope.launch {
+ viewState.emit(
+ currentViewState().copy(
+ isAppDefaultBrowser = defaultBrowserAlready,
+ showDefaultBrowserSetting = defaultWebBrowserCapability.deviceSupportsDefaultBrowserConfiguration(),
+ appTrackingProtectionOnboardingShown = appTrackingProtection.isOnboarded(),
+ appTrackingProtectionEnabled = appTrackingProtection.isRunning(),
+ emailAddress = emailManager.getEmailAddress(),
+ showAutofill = autofillCapabilityChecker.canAccessCredentialManagementScreen(),
+ showSyncSetting = deviceSyncState.isFeatureEnabled(),
+ isAutoconsentEnabled = autoconsent.isSettingEnabled(),
+ isPrivacyProEnabled = subscriptions.isEligible(),
+ isDuckPlayerEnabled = duckPlayer.getDuckPlayerState().let { it == ENABLED || it == DISABLED_WIH_HELP_LINK },
+ ),
+ )
+ }
+ }
+
+ // FIXME
+ // We need to fix this. This logic as inside the start method but it messes with the unit tests
+ // because when doing runningBlockingTest {} there is no delay and the tests crashes because this
+ // becomes a while(true) without any delay
+ private fun startPollingAppTPState() {
+ appTPPollJob += viewModelScope.launch(dispatcherProvider.io()) {
+ while (isActive) {
+ val isDeviceShieldEnabled = appTrackingProtection.isRunning()
+ val currentState = currentViewState()
+ viewState.value = currentState.copy(
+ appTrackingProtectionOnboardingShown = appTrackingProtection.isOnboarded(),
+ appTrackingProtectionEnabled = isDeviceShieldEnabled,
+ isPrivacyProEnabled = subscriptions.isEligible(),
+ )
+ delay(1_000)
+ }
+ }
+ }
+
+ fun viewState(): StateFlow {
+ return viewState
+ }
+
+ fun commands(): Flow {
+ return command.receiveAsFlow()
+ }
+
+ fun userRequestedToAddHomeScreenWidget() {
+ viewModelScope.launch { command.send(LaunchAddHomeScreenWidget) }
+ }
+
+ fun onDefaultBrowserSettingClicked() {
+ val defaultBrowserSelected = defaultWebBrowserCapability.isDefaultBrowser()
+ viewModelScope.launch {
+ viewState.emit(currentViewState().copy(isAppDefaultBrowser = defaultBrowserSelected))
+ command.send(LaunchDefaultBrowser)
+ }
+ pixel.fire(SETTINGS_DEFAULT_BROWSER_PRESSED)
+ }
+
+ fun onPrivateSearchSettingClicked() {
+ viewModelScope.launch { command.send(LaunchPrivateSearchWebPage) }
+ pixel.fire(SETTINGS_PRIVATE_SEARCH_PRESSED)
+ }
+
+ fun onWebTrackingProtectionSettingClicked() {
+ viewModelScope.launch { command.send(LaunchWebTrackingProtectionScreen) }
+ pixel.fire(SETTINGS_WEB_TRACKING_PROTECTION_PRESSED)
+ }
+
+ fun onCookiePopupProtectionSettingClicked() {
+ viewModelScope.launch { command.send(LaunchCookiePopupProtectionScreen) }
+ pixel.fire(SETTINGS_COOKIE_POPUP_PROTECTION_PRESSED)
+ }
+
+ fun onAutofillSettingsClick() {
+ viewModelScope.launch { command.send(LaunchAutofillSettings) }
+ }
+
+ fun onAccessibilitySettingClicked() {
+ viewModelScope.launch { command.send(LaunchAccessibilitySettings) }
+ pixel.fire(SETTINGS_ACCESSIBILITY_PRESSED)
+ }
+
+ fun onAboutSettingClicked() {
+ viewModelScope.launch { command.send(LaunchAboutScreen) }
+ pixel.fire(SETTINGS_ABOUT_PRESSED)
+ }
+
+ fun onGeneralSettingClicked() {
+ viewModelScope.launch { command.send(LaunchGeneralSettingsScreen) }
+ pixel.fire(SETTINGS_GENERAL_PRESSED)
+ }
+
+ fun onEmailProtectionSettingClicked() {
+ viewModelScope.launch {
+ val command = if (emailManager.isEmailFeatureSupported()) {
+ LaunchEmailProtection(EMAIL_PROTECTION_URL)
+ } else {
+ LaunchEmailProtectionNotSupported
+ }
+ this@NewSettingsViewModel.command.send(command)
+ }
+ pixel.fire(SETTINGS_EMAIL_PROTECTION_PRESSED)
+ }
+
+ fun onMacOsSettingClicked() {
+ viewModelScope.launch { command.send(LaunchMacOs) }
+ pixel.fire(SETTINGS_MAC_APP_PRESSED)
+ }
+
+ fun windowsSettingClicked() {
+ viewModelScope.launch {
+ command.send(LaunchWindows)
+ }
+ pixel.fire(SETTINGS_WINDOWS_APP_PRESSED)
+ }
+
+ fun onAppTPSettingClicked() {
+ viewModelScope.launch {
+ if (appTrackingProtection.isOnboarded()) {
+ command.send(LaunchAppTPTrackersScreen)
+ } else {
+ command.send(LaunchAppTPOnboarding)
+ }
+ pixel.fire(SETTINGS_APPTP_PRESSED)
+ }
+ }
+
+ private fun currentViewState(): ViewState {
+ return viewState.value
+ }
+
+ fun onSyncSettingClicked() {
+ viewModelScope.launch { command.send(LaunchSyncSettings) }
+ pixel.fire(SETTINGS_SYNC_PRESSED)
+ }
+
+ fun onFireButtonSettingClicked() {
+ viewModelScope.launch { command.send(LaunchFireButtonScreen) }
+ pixel.fire(SETTINGS_FIRE_BUTTON_PRESSED)
+ }
+
+ fun onPermissionsSettingClicked() {
+ viewModelScope.launch { command.send(LaunchPermissionsScreen) }
+ pixel.fire(SETTINGS_PERMISSIONS_PRESSED)
+ }
+
+ fun onAppearanceSettingClicked() {
+ viewModelScope.launch { command.send(LaunchAppearanceScreen) }
+ pixel.fire(SETTINGS_APPEARANCE_PRESSED)
+ }
+
+ fun onLaunchedFromNotification(pixelName: String) {
+ pixel.fire(pixelName)
+ }
+
+ companion object {
+ const val EMAIL_PROTECTION_URL = "https://duckduckgo.com/email"
+ }
+}
diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt
index 14e9a1f4f661..2414b4520263 100644
--- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt
+++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2017 DuckDuckGo
+ * 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.
@@ -16,421 +16,33 @@
package com.duckduckgo.app.settings
-import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.os.Bundle
-import android.view.View
-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.about.AboutScreenNoParams
-import com.duckduckgo.app.accessibility.AccessibilityScreens
-import com.duckduckgo.app.appearance.AppearanceScreen
-import com.duckduckgo.app.browser.BrowserActivity
-import com.duckduckgo.app.browser.R
-import com.duckduckgo.app.browser.databinding.ActivitySettingsBinding
-import com.duckduckgo.app.email.ui.EmailProtectionUnsupportedScreenNoParams
-import com.duckduckgo.app.firebutton.FireButtonScreenNoParams
-import com.duckduckgo.app.generalsettings.GeneralSettingsScreenNoParams
-import com.duckduckgo.app.global.view.launchDefaultAppActivity
-import com.duckduckgo.app.permissions.PermissionsScreenNoParams
-import com.duckduckgo.app.pixels.AppPixelName
-import com.duckduckgo.app.pixels.AppPixelName.PRIVACY_PRO_IS_ENABLED_AND_ELIGIBLE
-import com.duckduckgo.app.privatesearch.PrivateSearchScreenNoParams
-import com.duckduckgo.app.settings.SettingsViewModel.Command
-import com.duckduckgo.app.statistics.pixels.Pixel
-import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.Daily
-import com.duckduckgo.app.webtrackingprotection.WebTrackingProtectionScreenNoParams
-import com.duckduckgo.app.widget.AddWidgetLauncher
-import com.duckduckgo.appbuildconfig.api.AppBuildConfig
-import com.duckduckgo.autoconsent.impl.ui.AutoconsentSettingsActivity
-import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreen
-import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource
import com.duckduckgo.browser.api.ui.BrowserScreens.SettingsScreenNoParams
import com.duckduckgo.common.ui.DuckDuckGoActivity
-import com.duckduckgo.common.ui.view.gone
-import com.duckduckgo.common.ui.view.listitem.CheckListItem
-import com.duckduckgo.common.ui.view.listitem.TwoLineListItem
-import com.duckduckgo.common.ui.view.show
-import com.duckduckgo.common.ui.viewbinding.viewBinding
-import com.duckduckgo.common.utils.plugins.PluginPoint
import com.duckduckgo.di.scopes.ActivityScope
-import com.duckduckgo.internal.features.api.InternalFeaturePlugin
-import com.duckduckgo.macos.api.MacOsScreenWithEmptyParams
-import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerActivityWithEmptyParams
-import com.duckduckgo.mobile.android.app.tracking.ui.AppTrackingProtectionScreens.AppTrackerOnboardingActivityWithEmptyParamsParams
-import com.duckduckgo.navigation.api.GlobalActivityStarter
-import com.duckduckgo.settings.api.DuckPlayerSettingsPlugin
-import com.duckduckgo.settings.api.ProSettingsPlugin
-import com.duckduckgo.sync.api.SyncActivityWithEmptyParams
-import com.duckduckgo.windows.api.ui.WindowsScreenWithEmptyParams
+import com.duckduckgo.settings.api.NewSettingsFeature
import javax.inject.Inject
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import timber.log.Timber
@InjectWith(ActivityScope::class)
@ContributeToActivityStarter(SettingsScreenNoParams::class, screenName = "settings")
class SettingsActivity : DuckDuckGoActivity() {
- private val viewModel: SettingsViewModel by bindViewModel()
- private val binding: ActivitySettingsBinding by viewBinding()
-
- @Inject
- lateinit var pixel: Pixel
-
- @Inject
- lateinit var internalFeaturePlugins: PluginPoint
-
- @Inject
- lateinit var addWidgetLauncher: AddWidgetLauncher
-
- @Inject
- lateinit var appBuildConfig: AppBuildConfig
-
- @Inject
- lateinit var globalActivityStarter: GlobalActivityStarter
-
- @Inject
- lateinit var _proSettingsPlugin: PluginPoint
- private val proSettingsPlugin by lazy {
- _proSettingsPlugin.getPlugins()
- }
-
@Inject
- lateinit var _duckPlayerSettingsPlugin: PluginPoint
- private val duckPlayerSettingsPlugin by lazy {
- _duckPlayerSettingsPlugin.getPlugins()
- }
-
- private val viewsPrivacy
- get() = binding.includeSettings.contentSettingsPrivacy
-
- private val viewsSettings
- get() = binding.includeSettings.contentSettingsSettings
-
- private val viewsMore
- get() = binding.includeSettings.contentSettingsMore
-
- private val viewsInternal
- get() = binding.includeSettings.contentSettingsInternal
-
- private val viewsPro
- get() = binding.includeSettings.settingsSectionPro
+ lateinit var newSettingsFeature: NewSettingsFeature
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setContentView(binding.root)
- setupToolbar(binding.includeToolbar.toolbar)
-
- configureUiEventHandlers()
- configureInternalFeatures()
- configureSettings()
- lifecycle.addObserver(viewModel)
- observeViewModel()
-
- intent?.getStringExtra(BrowserActivity.LAUNCH_FROM_NOTIFICATION_PIXEL_NAME)?.let {
- viewModel.onLaunchedFromNotification(it)
- }
- }
-
- private fun configureUiEventHandlers() {
- with(viewsPrivacy) {
- setAsDefaultBrowserSetting.setClickListener { viewModel.onDefaultBrowserSettingClicked() }
- privateSearchSetting.setClickListener { viewModel.onPrivateSearchSettingClicked() }
- webTrackingProtectionSetting.setClickListener { viewModel.onWebTrackingProtectionSettingClicked() }
- cookiePopupProtectionSetting.setClickListener { viewModel.onCookiePopupProtectionSettingClicked() }
- emailSetting.setClickListener { viewModel.onEmailProtectionSettingClicked() }
- vpnSetting.setClickListener { viewModel.onAppTPSettingClicked() }
- }
-
- with(viewsSettings) {
- homeScreenWidgetSetting.setClickListener { viewModel.userRequestedToAddHomeScreenWidget() }
- autofillLoginsSetting.setClickListener { viewModel.onAutofillSettingsClick() }
- syncSetting.setClickListener { viewModel.onSyncSettingClicked() }
- fireButtonSetting.setClickListener { viewModel.onFireButtonSettingClicked() }
- permissionsSetting.setClickListener { viewModel.onPermissionsSettingClicked() }
- appearanceSetting.setClickListener { viewModel.onAppearanceSettingClicked() }
- accessibilitySetting.setClickListener { viewModel.onAccessibilitySettingClicked() }
- aboutSetting.setClickListener { viewModel.onAboutSettingClicked() }
- generalSetting.setClickListener { viewModel.onGeneralSettingClicked() }
- }
-
- with(viewsMore) {
- macOsSetting.setClickListener { viewModel.onMacOsSettingClicked() }
- windowsSetting.setClickListener { viewModel.windowsSettingClicked() }
- }
- }
-
- private fun configureSettings() {
- if (proSettingsPlugin.isEmpty()) {
- viewsPro.gone()
- } else {
- proSettingsPlugin.forEach { plugin ->
- viewsPro.addView(plugin.getView(this))
- }
- }
-
- if (duckPlayerSettingsPlugin.isEmpty()) {
- viewsSettings.settingsSectionDuckPlayer.gone()
- } else {
- duckPlayerSettingsPlugin.forEach { plugin ->
- viewsSettings.settingsSectionDuckPlayer.addView(plugin.getView(this))
- }
- }
- }
-
- private fun configureInternalFeatures() {
- viewsInternal.settingsSectionInternal.visibility = if (internalFeaturePlugins.getPlugins().isEmpty()) View.GONE else View.VISIBLE
- internalFeaturePlugins.getPlugins().forEach { feature ->
- Timber.v("Adding internal feature ${feature.internalFeatureTitle()}")
- val view = TwoLineListItem(this).apply {
- setPrimaryText(feature.internalFeatureTitle())
- setSecondaryText(feature.internalFeatureSubtitle())
- }
- viewsInternal.settingsInternalFeaturesContainer.addView(view)
- view.setClickListener { feature.onInternalFeatureClicked(this) }
- }
- }
-
- private fun observeViewModel() {
- viewModel.viewState()
- .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
- .distinctUntilChanged()
- .onEach { viewState ->
- viewState.let {
- updateDefaultBrowserViewVisibility(it)
- updateDeviceShieldSettings(
- it.appTrackingProtectionEnabled,
- it.appTrackingProtectionOnboardingShown,
- )
- updateEmailSubtitle(it.emailAddress)
- updateAutofill(it.showAutofill)
- updateSyncSetting(visible = it.showSyncSetting)
- updateAutoconsent(it.isAutoconsentEnabled)
- updatePrivacyPro(it.isPrivacyProEnabled)
- updateDuckPlayer(it.isDuckPlayerEnabled)
- }
- }.launchIn(lifecycleScope)
-
- viewModel.commands()
- .flowWithLifecycle(lifecycle, Lifecycle.State.CREATED)
- .onEach { processCommand(it) }
- .launchIn(lifecycleScope)
- }
-
- private fun updatePrivacyPro(isPrivacyProEnabled: Boolean) {
- if (isPrivacyProEnabled) {
- pixel.fire(PRIVACY_PRO_IS_ENABLED_AND_ELIGIBLE, type = Daily())
- viewsPro.show()
- } else {
- viewsPro.gone()
- }
- }
-
- private fun updateDuckPlayer(isDuckPlayerEnabled: Boolean) {
- if (isDuckPlayerEnabled) {
- viewsSettings.settingsSectionDuckPlayer.show()
+ if (newSettingsFeature.self().isEnabled()) {
+ startActivity(NewSettingsActivity.intent(this))
} else {
- viewsSettings.settingsSectionDuckPlayer.gone()
+ startActivity(LegacySettingsActivity.intent(this))
}
- }
-
- private fun updateAutofill(autofillEnabled: Boolean) = with(viewsSettings.autofillLoginsSetting) {
- visibility = if (autofillEnabled) {
- View.VISIBLE
- } else {
- View.GONE
- }
- }
-
- private fun updateEmailSubtitle(emailAddress: String?) {
- if (emailAddress.isNullOrEmpty()) {
- viewsPrivacy.emailSetting.setSecondaryText(getString(R.string.settingsEmailProtectionSubtitle))
- viewsPrivacy.emailSetting.setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
- } else {
- viewsPrivacy.emailSetting.setSecondaryText(emailAddress)
- viewsPrivacy.emailSetting.setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
- }
- }
-
- private fun updateSyncSetting(visible: Boolean) {
- with(viewsSettings.syncSetting) {
- isVisible = visible
- }
- }
-
- private fun updateAutoconsent(enabled: Boolean) {
- if (enabled) {
- viewsPrivacy.cookiePopupProtectionSetting.setSecondaryText(getString(R.string.cookiePopupProtectionEnabled))
- viewsPrivacy.cookiePopupProtectionSetting.setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
- } else {
- viewsPrivacy.cookiePopupProtectionSetting.setSecondaryText(getString(R.string.cookiePopupProtectionDescription))
- viewsPrivacy.cookiePopupProtectionSetting.setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
- }
- }
-
- private fun processCommand(it: Command?) {
- when (it) {
- is Command.LaunchDefaultBrowser -> launchDefaultAppScreen()
- is Command.LaunchAutofillSettings -> launchAutofillSettings()
- is Command.LaunchAccessibilitySettings -> launchAccessibilitySettings()
- is Command.LaunchAppTPTrackersScreen -> launchAppTPTrackersScreen()
- is Command.LaunchAppTPOnboarding -> launchAppTPOnboardingScreen()
- is Command.LaunchEmailProtection -> launchEmailProtectionScreen(it.url)
- is Command.LaunchEmailProtectionNotSupported -> launchEmailProtectionNotSupported()
- is Command.LaunchAddHomeScreenWidget -> launchAddHomeScreenWidget()
- is Command.LaunchMacOs -> launchMacOsScreen()
- is Command.LaunchWindows -> launchWindowsScreen()
- is Command.LaunchSyncSettings -> launchSyncSettings()
- is Command.LaunchPrivateSearchWebPage -> launchPrivateSearchScreen()
- is Command.LaunchWebTrackingProtectionScreen -> launchWebTrackingProtectionScreen()
- is Command.LaunchCookiePopupProtectionScreen -> launchCookiePopupProtectionScreen()
- is Command.LaunchFireButtonScreen -> launchFireButtonScreen()
- is Command.LaunchPermissionsScreen -> launchPermissionsScreen()
- is Command.LaunchAppearanceScreen -> launchAppearanceScreen()
- is Command.LaunchAboutScreen -> launchAboutScreen()
- is Command.LaunchGeneralSettingsScreen -> launchGeneralSettingsScreen()
- null -> TODO()
- }
- }
-
- private fun updateDefaultBrowserViewVisibility(it: SettingsViewModel.ViewState) {
- with(viewsPrivacy.setAsDefaultBrowserSetting) {
- visibility = if (it.showDefaultBrowserSetting) {
- if (it.isAppDefaultBrowser) {
- setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
- setSecondaryText(getString(R.string.settingsDefaultBrowserSetDescription))
- } else {
- setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
- setSecondaryText(getString(R.string.settingsDefaultBrowserNotSetDescription))
- }
- View.VISIBLE
- } else {
- View.GONE
- }
- }
- }
-
- private fun updateDeviceShieldSettings(
- appTPEnabled: Boolean,
- appTrackingProtectionOnboardingShown: Boolean,
- ) {
- with(viewsPrivacy) {
- if (appTPEnabled) {
- vpnSetting.setSecondaryText(getString(R.string.atp_SettingsDeviceShieldEnabled))
- vpnSetting.setItemStatus(CheckListItem.CheckItemStatus.ENABLED)
- } else {
- if (appTrackingProtectionOnboardingShown) {
- vpnSetting.setSecondaryText(getString(R.string.atp_SettingsDeviceShieldDisabled))
- vpnSetting.setItemStatus(CheckListItem.CheckItemStatus.WARNING)
- } else {
- vpnSetting.setSecondaryText(getString(R.string.atp_SettingsDeviceShieldNeverEnabled))
- vpnSetting.setItemStatus(CheckListItem.CheckItemStatus.DISABLED)
- }
- }
- }
- }
-
- private fun launchDefaultAppScreen() {
- launchDefaultAppActivity()
- }
-
- private fun launchAutofillSettings() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, AutofillSettingsScreen(source = AutofillSettingsLaunchSource.SettingsActivity), options)
- }
-
- private fun launchAccessibilitySettings() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, AccessibilityScreens.Default, options)
- }
-
- private fun launchEmailProtectionScreen(url: String) {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- startActivity(BrowserActivity.intent(this, url, interstitialScreen = true), options)
- this.finish()
- }
-
- private fun launchEmailProtectionNotSupported() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, EmailProtectionUnsupportedScreenNoParams, options)
- }
-
- private fun launchMacOsScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, MacOsScreenWithEmptyParams, options)
- }
-
- private fun launchWindowsScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, WindowsScreenWithEmptyParams, options)
- }
-
- private fun launchSyncSettings() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, SyncActivityWithEmptyParams, options)
- }
-
- private fun launchAppTPTrackersScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, AppTrackerActivityWithEmptyParams, options)
- }
-
- private fun launchAppTPOnboardingScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, AppTrackerOnboardingActivityWithEmptyParamsParams, options)
- }
-
- private fun launchAddHomeScreenWidget() {
- pixel.fire(AppPixelName.SETTINGS_ADD_HOME_SCREEN_WIDGET_CLICKED)
- addWidgetLauncher.launchAddWidget(this)
- }
-
- private fun launchPrivateSearchScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, PrivateSearchScreenNoParams, options)
- }
-
- private fun launchWebTrackingProtectionScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, WebTrackingProtectionScreenNoParams, options)
- }
-
- private fun launchCookiePopupProtectionScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- startActivity(AutoconsentSettingsActivity.intent(this), options)
- }
-
- private fun launchFireButtonScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, FireButtonScreenNoParams, options)
- }
-
- private fun launchPermissionsScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, PermissionsScreenNoParams, options)
- }
-
- private fun launchAppearanceScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, AppearanceScreen.Default, options)
- }
-
- private fun launchAboutScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, AboutScreenNoParams, options)
- }
-
- private fun launchGeneralSettingsScreen() {
- val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle()
- globalActivityStarter.start(this, GeneralSettingsScreenNoParams, options)
+ finish()
}
companion object {
diff --git a/app/src/main/res/layout/activity_settings_new.xml b/app/src/main/res/layout/activity_settings_new.xml
new file mode 100644
index 000000000000..94b486d9afa3
--- /dev/null
+++ b/app/src/main/res/layout/activity_settings_new.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/test/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/settings/LegacySettingsViewModelTest.kt
similarity index 98%
rename from app/src/test/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt
rename to app/src/test/java/com/duckduckgo/app/settings/LegacySettingsViewModelTest.kt
index 4d76c398eaf8..48f7c9e6d36f 100644
--- a/app/src/test/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt
+++ b/app/src/test/java/com/duckduckgo/app/settings/LegacySettingsViewModelTest.kt
@@ -20,8 +20,8 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import app.cash.turbine.test
import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector
import com.duckduckgo.app.pixels.AppPixelName
-import com.duckduckgo.app.settings.SettingsViewModel.Command
-import com.duckduckgo.app.settings.SettingsViewModel.Companion.EMAIL_PROTECTION_URL
+import com.duckduckgo.app.settings.LegacySettingsViewModel.Command
+import com.duckduckgo.app.settings.LegacySettingsViewModel.Companion.EMAIL_PROTECTION_URL
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.autoconsent.api.Autoconsent
import com.duckduckgo.autofill.api.AutofillCapabilityChecker
@@ -52,7 +52,7 @@ class SettingsViewModelTest {
@Suppress("unused")
var instantTaskExecutorRule = InstantTaskExecutorRule()
- private lateinit var testee: SettingsViewModel
+ private lateinit var testee: LegacySettingsViewModel
@Mock
private lateinit var mockDefaultBrowserDetector: DefaultBrowserDetector
@@ -95,7 +95,7 @@ class SettingsViewModelTest {
whenever(subscriptions.isEligible()).thenReturn(true)
}
- testee = SettingsViewModel(
+ testee = LegacySettingsViewModel(
mockDefaultBrowserDetector,
appTrackingProtection,
mockPixel,
diff --git a/settings/settings-api/build.gradle b/settings/settings-api/build.gradle
index 806b9cdd3d98..65619c220529 100644
--- a/settings/settings-api/build.gradle
+++ b/settings/settings-api/build.gradle
@@ -22,6 +22,9 @@ plugins {
apply from: "$rootProject.projectDir/gradle/android-library.gradle"
dependencies {
+ /* Temporary while developing new settings screen */
+ implementation project(':feature-toggles-api')
+
implementation project(':navigation-api')
implementation Google.dagger
implementation AndroidX.core.ktx
diff --git a/settings/settings-api/src/main/java/com/duckduckgo/settings/api/NewSettingsFeature.kt b/settings/settings-api/src/main/java/com/duckduckgo/settings/api/NewSettingsFeature.kt
new file mode 100644
index 000000000000..31a7694db581
--- /dev/null
+++ b/settings/settings-api/src/main/java/com/duckduckgo/settings/api/NewSettingsFeature.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.settings.api
+
+import com.duckduckgo.feature.toggles.api.Toggle
+
+interface NewSettingsFeature {
+
+ @Toggle.DefaultValue(false)
+ fun self(): Toggle
+}
diff --git a/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/NewSettingsTrigger.kt b/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/NewSettingsTrigger.kt
new file mode 100644
index 000000000000..86f4cd479db0
--- /dev/null
+++ b/settings/settings-impl/src/main/java/com/duckduckgo/settings/impl/NewSettingsTrigger.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.settings.impl
+
+import com.duckduckgo.anvil.annotations.ContributesRemoteFeature
+import com.duckduckgo.di.scopes.AppScope
+import com.duckduckgo.settings.api.NewSettingsFeature
+
+@ContributesRemoteFeature(
+ scope = AppScope::class,
+ featureName = "newSettings",
+ boundType = NewSettingsFeature::class,
+)
+private interface NewSettingsCodeGenTrigger