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