From ae3949c7ac009237597b96b7b002f2a7c3ec0e40 Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Sun, 14 Jul 2024 23:23:51 +0100 Subject: [PATCH 01/20] Update fastlane to allow for updating release notes of an existing release in Play Store (#4758) Task/Issue URL: https://app.asana.com/0/608920331025315/1207796666154886/f ### Description Adds a new fastlane script which can be used to update release notes on the Play Store for a particular app version. Typically, this will be executed from CI. Example usage: `fastlane update_release_notes_playstore release_number:5.208.0 release_notes:"Bug fixes and other improvements"` ### Steps to test this PR QA-optional --- fastlane/Fastfile | 70 ++++++++++++++++++++++++++++++++++++++++------ fastlane/README.md | 8 ++++++ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5a3c943c2540..80fdf1b08a36 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -55,12 +55,13 @@ platform :android do desc "Upload APK to Play Store, in production track with a very small rollout percentage" lane :deploy_playstore do - update_fastlane_release_notes() - props = property_file_read(file: "app/version/version.properties") version = props["VERSION"] + flversion = convert_version(release_number: version) apkPath = "app/build/outputs/apk/play/release/duckduckgo-#{version}-play-release.apk" + update_fastlane_release_notes(release_number: flversion) + upload_to_play_store( apk: apkPath, track: 'production', @@ -70,11 +71,53 @@ platform :android do validate_only: false ) - cleanup_fastlane_release_notes() + cleanup_fastlane_release_notes(release_number: flversion) annotate_release() end + + desc "Update Play Store release notes" + lane :update_release_notes_playstore do |options| + + options_release_number = options[:release_number] + options_release_notes = options[:release_notes] + options_notes_type = options[:notes_type] + + newVersion = determine_version_number( + release_number: options_release_number + ) + releaseNotes = determine_release_notes( + release_notes: options_release_notes, + notes_type: options_notes_type + ) + + formattedPlayStore = "#{releaseNotesBodyHeader}\n#{releaseNotes}" + validateReleaseNotes(releaseNotes: formattedPlayStore) + UI.message("\n#{formattedPlayStore}") + + flversion = convert_version(release_number: newVersion) + + releaseNotesLocales.each do |locale| + File.open("../fastlane/metadata/android/#{locale}/changelogs/#{flversion}.txt", 'w') do |file| file.write("#{formattedPlayStore}") end + end + + upload_to_play_store( + version_code: flversion, + skip_upload_apk: true, + skip_upload_aab: true, + skip_upload_metadata: true, + skip_upload_changelogs: false, + skip_upload_images: true, + skip_upload_screenshots: true, + validate_only: false + ) + + cleanup_fastlane_release_notes(release_number: flversion) + + end + + desc "Annotate release" private_lane :annotate_release do props = property_file_read(file: "app/version/version.properties") @@ -142,11 +185,10 @@ platform :android do end desc "Update local changelist metadata" - private_lane :update_fastlane_release_notes do - + private_lane :update_fastlane_release_notes do |options| + flversion = options[:release_number] releaseNotes = release_notes_playstore() - flversion=gradle(task: '-q fastlaneVersionCode').lines.map(&:chomp)[0] UI.message("App version for fastlane is #{flversion}.\nRelease notes for Play Store:\n\n#{releaseNotes}") releaseNotesLocales.each do |locale| @@ -156,9 +198,9 @@ platform :android do end desc "Clean up local changelist metadata" - private_lane :cleanup_fastlane_release_notes do + private_lane :cleanup_fastlane_release_notes do |options| - flversion=gradle(task: '-q fastlaneVersionCode').lines.map(&:chomp)[0] + flversion = options[:release_number] releaseNotesLocales.each do |locale| sh("rm '../fastlane/metadata/android/#{locale}/changelogs/#{flversion}.txt'") @@ -386,4 +428,16 @@ platform :android do end end + private_lane :"convert_version" do |options| + original = options[:release_number] + major, minor, patch = original.downcase.split('.') + major = major.to_i + minor = minor.to_i + patch = patch.to_i + (major * 10_000_000) + (minor * 10_000) + (patch * 1_000) + end + + + + end \ No newline at end of file diff --git a/fastlane/README.md b/fastlane/README.md index 375d4b18ae12..c48a212559ed 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -23,6 +23,14 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do Upload APK to Play Store, in production track with a very small rollout percentage +### android update_release_notes_playstore + +```sh +[bundle exec] fastlane android update_release_notes_playstore +``` + +Update Play Store release notes + ### android deploy_dogfood ```sh From a83e02a06ee48d9712053d6145d960b32d337ccb Mon Sep 17 00:00:00 2001 From: Josh Leibstein Date: Mon, 15 Jul 2024 13:27:30 +0100 Subject: [PATCH 02/20] Fix selected text search back navigation (#4761) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/488551667048375/1207778360497709/f ### Description Closes the current tab when searching selected text and pressing back. ### Steps to test this PR - [x] Select some text in the browser - [x] Click the 3 dots and select “Search DuckDuckGo" - [x] Verify that a new tab is opened for the selected text - [x] Go back - [x] Verify that the tab is closed and the previous tab is shown --- .../java/com/duckduckgo/app/SelectedTextSearchActivity.kt | 2 +- .../java/com/duckduckgo/app/browser/BrowserActivity.kt | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/SelectedTextSearchActivity.kt b/app/src/main/java/com/duckduckgo/app/SelectedTextSearchActivity.kt index dd92d0ee8b7b..8a0626d71180 100644 --- a/app/src/main/java/com/duckduckgo/app/SelectedTextSearchActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/SelectedTextSearchActivity.kt @@ -33,7 +33,7 @@ class SelectedTextSearchActivity : AppCompatActivity() { super.onCreate(savedInstanceState) val query = extractQuery(intent) - startActivity(BrowserActivity.intent(this, queryExtra = query)) + startActivity(BrowserActivity.intent(this, queryExtra = query, selectedText = true)) finish() } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index 276e7cd6d927..9a0717d8c7cb 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -347,7 +347,10 @@ open class BrowserActivity : DuckDuckGoActivity() { return } else { Timber.w("opening in new tab requested for $sharedText") - lifecycleScope.launch { viewModel.onOpenInNewTabRequested(query = sharedText, skipHome = true) } + val selectedText = intent.getBooleanExtra(SELECTED_TEXT_EXTRA, false) + val sourceTabId = if (selectedText) currentTab?.tabId else null + val skipHome = !selectedText + lifecycleScope.launch { viewModel.onOpenInNewTabRequested(sourceTabId = sourceTabId, query = sharedText, skipHome = skipHome) } return } } @@ -531,12 +534,14 @@ open class BrowserActivity : DuckDuckGoActivity() { newSearch: Boolean = false, notifyDataCleared: Boolean = false, openInCurrentTab: Boolean = false, + selectedText: Boolean = false, ): Intent { val intent = Intent(context, BrowserActivity::class.java) intent.putExtra(EXTRA_TEXT, queryExtra) intent.putExtra(NEW_SEARCH_EXTRA, newSearch) intent.putExtra(NOTIFY_DATA_CLEARED_EXTRA, notifyDataCleared) intent.putExtra(OPEN_IN_CURRENT_TAB_EXTRA, openInCurrentTab) + intent.putExtra(SELECTED_TEXT_EXTRA, selectedText) return intent } @@ -547,6 +552,7 @@ open class BrowserActivity : DuckDuckGoActivity() { const val LAUNCH_FROM_FAVORITES_WIDGET = "LAUNCH_FROM_FAVORITES_WIDGET" const val LAUNCH_FROM_NOTIFICATION_PIXEL_NAME = "LAUNCH_FROM_NOTIFICATION_PIXEL_NAME" const val OPEN_IN_CURRENT_TAB_EXTRA = "OPEN_IN_CURRENT_TAB_EXTRA" + const val SELECTED_TEXT_EXTRA = "SELECTED_TEXT_EXTRA" private const val APP_ENJOYMENT_DIALOG_TAG = "AppEnjoyment" From b0636754a1aef1625abc134d6d98284d0fb8b757 Mon Sep 17 00:00:00 2001 From: Josh Leibstein Date: Mon, 15 Jul 2024 13:46:45 +0100 Subject: [PATCH 03/20] Update Anvil to 2.5.0-beta09 (#4754) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1200204095367872/1207786068216975/f ### Description Updates Anvil `2.5.0-beta07` → `2.5.0-beta09` End-to-end test run: https://github.com/duckduckgo/Android/actions/runs/9894234137 ✅ Nightly test run: https://github.com/duckduckgo/Android/actions/runs/9909474905 ✅ ### Steps to test this PR - [x] Smoke test --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a62f60068a25..9d70c6104ff3 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { ext { kotlin_version = '1.9.22' spotless = '6.1.2' - anvil_version = '2.5.0-beta07' + anvil_version = '2.5.0-beta09' ksp_version = '1.9.22-1.0.17' gradle_plugin = '8.2.0' min_sdk = 26 From dd662960e9b9bd2b6e8ff78cd4884c76fc94f765 Mon Sep 17 00:00:00 2001 From: Josh Leibstein Date: Mon, 15 Jul 2024 14:06:26 +0100 Subject: [PATCH 04/20] Update AndroidX versions (#4756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1200204095367872/1207761853531200/f ### Description Brings AndroidX dependencies up-to-date: ``` - androidx.browser=1.7.0 → 1.8.0 - androidx.core=1.12.0 → 1.13.1 - androidx.fragment=1.5.2 → 1.8.1 - androidx.lifecycle=2.5.1 → 2.8.3 - androidx.test.orchestrator=1.4.2 → 1.5.0 - androidx.test.espresso=3.5.0 → 3.6.1 - androidx.test.ext.junit=1.1.4 → 1.2.1 - androidx.test.rules=1.5.0 → 1.6.1 - androidx.test.runner=1.5.1 → 1.6.1 - androidx.webkit=1.9.0 → 1.11.0 - androidx.work=2.7.1 → 2.9.0 - androidx.viewpager2=1.0.0 → 1.1.0 ``` **Test runs** - End-to-end: https://github.com/duckduckgo/Android/actions/runs/9909564980 ✅ - Nightly: https://github.com/duckduckgo/Android/actions/runs/9909423866 ✅ - Custom tabs: https://github.com/duckduckgo/Android/actions/runs/9909541427 ✅ - Design system: https://github.com/duckduckgo/Android/actions/runs/9909810696 ✅ - Autofill: https://github.com/duckduckgo/Android/actions/runs/9909863851 ✅ ### Steps to test this PR - [x] Smoke test --- .../ui/ManageRecentAppsProtectionActivity.kt | 1 + ...TrackingProtectionExclusionListActivity.kt | 1 + .../breakage/ReportBreakageAppListActivity.kt | 1 + .../ui/onboarding/VpnOnboardingActivity.kt | 1 + .../ui/report/DeviceShieldAppTrackersInfo.kt | 1 + .../AppTPCompanyTrackersActivity.kt | 1 + .../DeviceShieldMostRecentActivity.kt | 1 + .../DeviceShieldTrackerActivity.kt | 1 + ...ataClearerForegroundAppRestartPixelTest.kt | 8 ++++++ .../feedback/ui/common/FeedbackActivity.kt | 1 + .../com/duckduckgo/app/fire/FireActivity.kt | 4 --- .../app/onboarding/ui/OnboardingActivity.kt | 1 + .../app/survey/ui/SurveyActivity.kt | 1 + .../ui/AddWidgetInstructionsActivity.kt | 1 + versions.properties | 26 +++++++++---------- 15 files changed, 33 insertions(+), 17 deletions(-) diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/ManageRecentAppsProtectionActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/ManageRecentAppsProtectionActivity.kt index 793acb66048e..4905a4e5c63d 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/ManageRecentAppsProtectionActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/ManageRecentAppsProtectionActivity.kt @@ -243,6 +243,7 @@ class ManageRecentAppsProtectionActivity : } override fun onBackPressed() { + super.onBackPressed() onSupportNavigateUp() } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/TrackingProtectionExclusionListActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/TrackingProtectionExclusionListActivity.kt index b7aa73be6e4a..1e9432f24555 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/TrackingProtectionExclusionListActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/apps/ui/TrackingProtectionExclusionListActivity.kt @@ -274,6 +274,7 @@ class TrackingProtectionExclusionListActivity : } override fun onBackPressed() { + super.onBackPressed() onSupportNavigateUp() } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/breakage/ReportBreakageAppListActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/breakage/ReportBreakageAppListActivity.kt index fc804a7d8680..401eedcca29c 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/breakage/ReportBreakageAppListActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/breakage/ReportBreakageAppListActivity.kt @@ -117,6 +117,7 @@ class ReportBreakageAppListActivity : DuckDuckGoActivity(), ReportBreakageAppLis } override fun onBackPressed() { + super.onBackPressed() onSupportNavigateUp() } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnOnboardingActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnOnboardingActivity.kt index f41abbeca5c6..650f49a232d4 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnOnboardingActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/onboarding/VpnOnboardingActivity.kt @@ -170,6 +170,7 @@ class VpnOnboardingActivity : DuckDuckGoActivity() { } override fun onBackPressed() { + super.onBackPressed() // go back to previous screen or get out if first page onSupportNavigateUp() } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/report/DeviceShieldAppTrackersInfo.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/report/DeviceShieldAppTrackersInfo.kt index 2fcbe4c208f3..7825961728ae 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/report/DeviceShieldAppTrackersInfo.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/report/DeviceShieldAppTrackersInfo.kt @@ -50,6 +50,7 @@ class DeviceShieldAppTrackersInfo : DuckDuckGoActivity() { } override fun onBackPressed() { + super.onBackPressed() finish() } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/AppTPCompanyTrackersActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/AppTPCompanyTrackersActivity.kt index ea44bc42f80e..10b8d16b6e77 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/AppTPCompanyTrackersActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/AppTPCompanyTrackersActivity.kt @@ -257,6 +257,7 @@ class AppTPCompanyTrackersActivity : DuckDuckGoActivity() { } override fun onBackPressed() { + super.onBackPressed() finish() } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldMostRecentActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldMostRecentActivity.kt index 913658e91d18..6ee6e93a284f 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldMostRecentActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldMostRecentActivity.kt @@ -38,6 +38,7 @@ class DeviceShieldMostRecentActivity : DuckDuckGoActivity() { } override fun onBackPressed() { + super.onBackPressed() finish() } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt index 2f6a514e0672..1620e44ceaa3 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/tracker_activity/DeviceShieldTrackerActivity.kt @@ -249,6 +249,7 @@ class DeviceShieldTrackerActivity : } override fun onBackPressed() { + super.onBackPressed() onSupportNavigateUp() } diff --git a/app/src/androidTest/java/com/duckduckgo/app/fire/DataClearerForegroundAppRestartPixelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/fire/DataClearerForegroundAppRestartPixelTest.kt index 849da49fb243..a79e9914a2c8 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/fire/DataClearerForegroundAppRestartPixelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/fire/DataClearerForegroundAppRestartPixelTest.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.fire +import android.content.Context import android.content.Intent import android.net.Uri import androidx.lifecycle.LifecycleOwner @@ -24,6 +25,7 @@ import com.duckduckgo.app.browser.BrowserActivity import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.systemsearch.SystemSearchActivity +import org.junit.Before import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -34,6 +36,12 @@ class DataClearerForegroundAppRestartPixelTest { private val pixel = mock() private val testee = DataClearerForegroundAppRestartPixel(context, pixel) + @Before + fun setUp() { + val preferences = context.getSharedPreferences(DataClearerForegroundAppRestartPixel.FILENAME, Context.MODE_PRIVATE) + preferences.edit().clear().apply() + } + @Test fun whenAppRestartsAfterOpenSearchWidgetThenPixelWithIntentIsSent() { val intent = SystemSearchActivity.fromWidget(context) diff --git a/app/src/main/java/com/duckduckgo/app/feedback/ui/common/FeedbackActivity.kt b/app/src/main/java/com/duckduckgo/app/feedback/ui/common/FeedbackActivity.kt index 5e68f44e908f..7480561308ed 100644 --- a/app/src/main/java/com/duckduckgo/app/feedback/ui/common/FeedbackActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/feedback/ui/common/FeedbackActivity.kt @@ -154,6 +154,7 @@ class FeedbackActivity : } override fun onBackPressed() { + super.onBackPressed() viewModel.onBackPressed() } diff --git a/app/src/main/java/com/duckduckgo/app/fire/FireActivity.kt b/app/src/main/java/com/duckduckgo/app/fire/FireActivity.kt index ad590a042093..93389b3dfadf 100644 --- a/app/src/main/java/com/duckduckgo/app/fire/FireActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/fire/FireActivity.kt @@ -47,10 +47,6 @@ class FireActivity : AppCompatActivity() { killProcess() } - override fun onBackPressed() { - // do nothing - the activity will kill itself soon enough - } - companion object { private const val KEY_RESTART_INTENTS = "KEY_RESTART_INTENTS" diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt index 643b9d8867c2..43786cbf0547 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/OnboardingActivity.kt @@ -69,6 +69,7 @@ class OnboardingActivity : DuckDuckGoActivity() { } override fun onBackPressed() { + super.onBackPressed() val currentPage = viewPager.currentItem if (currentPage == 0) { finish() diff --git a/app/src/main/java/com/duckduckgo/app/survey/ui/SurveyActivity.kt b/app/src/main/java/com/duckduckgo/app/survey/ui/SurveyActivity.kt index 14c88cd5d0ff..916a537a787b 100644 --- a/app/src/main/java/com/duckduckgo/app/survey/ui/SurveyActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/survey/ui/SurveyActivity.kt @@ -127,6 +127,7 @@ class SurveyActivity : DuckDuckGoActivity() { } override fun onBackPressed() { + super.onBackPressed() if (webView.canGoBack()) { webView.goBack() } else { diff --git a/app/src/main/java/com/duckduckgo/app/widget/ui/AddWidgetInstructionsActivity.kt b/app/src/main/java/com/duckduckgo/app/widget/ui/AddWidgetInstructionsActivity.kt index 7cfe3bd0ea86..ed32a6682f51 100644 --- a/app/src/main/java/com/duckduckgo/app/widget/ui/AddWidgetInstructionsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/widget/ui/AddWidgetInstructionsActivity.kt @@ -63,6 +63,7 @@ class AddWidgetInstructionsActivity : DuckDuckGoActivity() { } override fun onBackPressed() { + super.onBackPressed() viewModel.onClosePressed() } diff --git a/versions.properties b/versions.properties index 988a41b55e2e..061582285b0f 100644 --- a/versions.properties +++ b/versions.properties @@ -13,7 +13,7 @@ version.androidx.appcompat=1.7.0 version.androidx.arch.core=2.2.0 -version.androidx.browser=1.7.0 +version.androidx.browser=1.8.0 version.androidx.constraintlayout=2.1.4 @@ -25,31 +25,31 @@ version.androidx.localbroadcastmanager=1.1.0 version.androidx.recyclerview=1.3.2 -version.androidx.core=1.12.0 +version.androidx.core=1.13.1 -version.androidx.fragment=1.5.2 +version.androidx.fragment=1.8.1 version.androidx.legacy=1.0.0 -version.androidx.lifecycle=2.5.1 +version.androidx.lifecycle=2.8.3 version.androidx.room=2.6.1 version.androidx.swiperefreshlayout=1.1.0 -version.androidx.test.orchestrator=1.4.2 +version.androidx.test.orchestrator=1.5.0 -version.androidx.test.espresso=3.5.0 +version.androidx.test.espresso=3.6.1 -version.androidx.test.ext.junit=1.1.4 +version.androidx.test.ext.junit=1.2.1 -version.androidx.test.rules=1.5.0 +version.androidx.test.rules=1.6.1 -version.androidx.test.runner=1.5.1 +version.androidx.test.runner=1.6.1 -version.androidx.webkit=1.9.0 +version.androidx.webkit=1.11.0 -version.androidx.work=2.7.1 +version.androidx.work=2.9.0 version.androidx.security-crypto=1.1.0-alpha06 @@ -101,7 +101,7 @@ version.okhttp3=4.12.0 version.net.zetetic..android-database-sqlcipher=4.5.1 - version.okio=3.2.0 +version.okio=3.2.0 version.org.apache.commons..commons-math3=3.6.1 @@ -117,7 +117,7 @@ version.org.jsoup..jsoup=1.15.3 version.com.jakewharton.retrofit..retrofit2-kotlin-coroutines-adapter=0.9.2 -version.androidx.viewpager2=1.0.0 +version.androidx.viewpager2=1.1.0 version.robolectric=4.12.2 From 6998a633390b2709b5ce8c23e308dc594fe3de2d Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Mon, 15 Jul 2024 14:16:54 +0100 Subject: [PATCH 05/20] Translations for autofill import via desktop sync (#4729) Task/Issue URL: https://app.asana.com/0/72649045549333/1207655254348190/f ### Description Translations ### Steps to test this PR - [x] CI Passes QA-optional --------- Co-authored-by: Marcos Holgado --- .../res/values-bg/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-cs/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-da/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-de/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-el/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-es/strings-autofill-impl.xml | 30 ++++++++++++++--- .../res/values-et/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-fi/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-fr/strings-autofill-impl.xml | 28 ++++++++++++++-- .../res/values-hr/strings-autofill-impl.xml | 24 +++++++++++++- .../res/values-hu/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-it/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-lt/strings-autofill-impl.xml | 24 +++++++++++++- .../res/values-lv/strings-autofill-impl.xml | 24 +++++++++++++- .../res/values-nb/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-nl/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-pl/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-pt/strings-autofill-impl.xml | 24 +++++++++++++- .../res/values-ro/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-ru/strings-autofill-impl.xml | 24 +++++++++++++- .../res/values-sk/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-sl/strings-autofill-impl.xml | 24 +++++++++++++- .../res/values-sv/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../res/values-tr/strings-autofill-impl.xml | 32 ++++++++++++++++--- .../src/main/res/values/donottranslate.xml | 20 ------------ .../main/res/values/strings-autofill-impl.xml | 32 ++++++++++++++++--- 26 files changed, 648 insertions(+), 118 deletions(-) diff --git a/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml index 8ce2da71c2af..6e6d5b394be2 100644 --- a/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-bg/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Настройване в Настройки Запазване на паролата @@ -168,27 +168,49 @@ Управление на пароли Вашата парола ще бъде изтрита от това устройство. - + Вашата парола ще бъде изтрита от това устройство. Вашите пароли ще бъдат изтрити от това устройство. Вашата парола ще бъде изтрита от всички синхронизирани устройства. - + Вашата парола ще бъде изтрита от всички синхронизирани устройства. Вашите пароли ще бъдат изтрити от всички синхронизирани устройства. Сигурни ли сте, че искате да изтриете тази парола? - + Сигурни ли сте, че искате да изтриете %1$d парола? Сигурни ли сте, че искате да изтриете %1$d пароли? Уверете се, че все още имате друг начин за достъп до акаунта си. - + Уверете се, че все още имате друг начин за достъп до Вашия акаунт. Уверете се, че все още имате друг начин за достъп до Вашите акаунти. + Паролите от други браузъри или приложения могат да бъдат импортирани чрез версията за работен плот на браузъра DuckDuckGo. + + Импортиране на пароли + Как се импортират пароли + Импортирайте пароли в десктоп версията на браузъра DuckDuckGo, след което ги синхронизирайте с различни устройства. + Вземете браузъра за работен плот + Връзката е копирана + Вземете браузъра DuckDuckGo за Mac или Windows + Търсете поверително и блокирайте тракерите с браузъра за настолен компютър DuckDuckGo. Посетете тази връзка на своя компютър, за да го изтеглите още днес.\n\n%1$s + Синхронизиране с браузъра на работния плот + Импортиране от браузъра на работния плот: + Отворете DuckDuckGo на Mac или Windows + "Настройки > Пароли]]>" + Импортиране на пароли… и следвайте стъпките за импортиране]]> + След като бъдат импортирани на вашия компютър, можете да настроите синхронизирането на това устройство + + Изтеглете приложението за настолен компютър + Вземете DuckDuckGo за Mac или Windows + На компютъра си отидете на: + duckduckgo.com/browser + Споделяне на връзка за изтегляне + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml index 5ab7473d951d..de4e549ae820 100644 --- a/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-cs/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Nastavit v Nastaveních Uložit heslo @@ -172,7 +172,7 @@ Spravovat hesla Tvoje heslo se z tohohle zařízení smaže. - + Tvoje heslo se z tohohle zařízení smaže. Tvoje hesla se z tohohle zařízení smažou. Tvoje hesla se z tohohle zařízení smažou. @@ -180,7 +180,7 @@ Tvoje heslo se smaže ze všech synchronizovaných zařízení. - + Tvoje heslo se smaže ze všech synchronizovaných zařízení. Tvoje hesla se smažou ze všech synchronizovaných zařízení. Tvoje hesla se smažou ze všech synchronizovaných zařízení. @@ -188,7 +188,7 @@ Opravdu chceš smazat tohle heslo? - + Opravdu chceš smazat %1$d heslo? Opravdu chceš smazat %1$d hesla? Opravdu chceš smazat %1$d hesla? @@ -196,11 +196,33 @@ Zkontroluj si předtím, že se i tak dostaneš ke svému účtu. - + Zkontroluj si předtím, že se k účtu i tak dostaneš. Zkontroluj si předtím, že se k účtům i tak dostaneš. Zkontroluj si předtím, že se k účtům i tak dostaneš. Zkontroluj si předtím, že se k účtům i tak dostaneš. + Hesla z jiných prohlížečů nebo aplikací se dají importovat pomocí verze prohlížeče DuckDuckGo pro počítače. + + Importovat hesla + Jak importovat hesla + Importuj hesla do desktopové verze prohlížeče DuckDuckGo a potom je synchronizuj mezi zařízeními. + Nainstalovat prohlížeč pro počítač + Odkaz se zkopíroval + Nainstaluj si prohlížeč DuckDuckGo pro Mac nebo Windows + Vyhledávej soukromě a blokuj trackery pomocí prohlížeče DuckDuckGo pro počítače. Stáhneš si ho z tohohle odkazu.\n\n%1$s + Synchronizovat s počítačem + Import z prohlížeče na počítači: + Spusť DuckDuckGo na systému Mac nebo Windows + "Nastavení > Hesla]]>" + Importovat hesla… a postupuj podle pokynů pro import.]]> + Po importu hesel do počítače můžeš na tomhle zařízení nastavit synchronizaci + + Získat aplikaci pro počítač + Nainstaluj si DuckDuckGo pro Mac nebo Windows + V počítači přejdi na: + duckduckgo.com/browser + Sdílet odkaz ke stažení + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml index 92df5c6073f4..013826f7eb65 100644 --- a/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-da/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Konfigurer i Indstillinger Gem adgangskode @@ -168,27 +168,49 @@ Administrer adgangskoder Din adgangskode slettes fra denne enhed. - + Din adgangskode slettes fra denne enhed. Dine adgangskoder slettes fra denne enhed. Din adgangskode slettes fra alle synkroniserede enheder. - + Din adgangskode slettes fra alle synkroniserede enheder. Dine adgangskoder slettes fra alle synkroniserede enheder. Er du sikker på, at du vil slette denne adgangskode? - + Er du sikker på, at du vil slette %1$d adgangskode? Er du sikker på, at du vil slette %1$d adgangskoder? Husk at sikre, at du stadig har adgang til din konto. - + Husk at sikre, at du stadig har adgang til din konto. Husk at sikre, at du stadig har adgang til dine konti. + Adgangskoder fra andre browsere eller apps kan importeres ved hjælp af computerversionen af DuckDuckGo-browseren. + + Importer adgangskoder + Sådan importerer du adgangskoder + Importer adgangskoder i computerversionen af DuckDuckGo-browseren, og synkroniser derefter på tværs af enheder. + Hent browser til computeren + Link kopieret + Hent DuckDuckGo-browseren til Mac eller Windows + Søg privat og bloker trackere med DuckDuckGo-browseren til computeren. Besøg dette link på din computer for at downloade i dag.\n\n%1$s + Synkroniser med computer + Importer fra browseren på computeren: + Åbn DuckDuckGo på Mac eller Windows + "Indstillinger > Adgangskoder]]>" + Importer adgangskoder... og følg trinnene for at importere]]> + Når de er importeret til din computer, kan du konfigurere synkronisering på denne enhed + + Hent skrivebordsapp + Hent DuckDuckGo til Mac eller Windows + På din computer skal du gå til: + duckduckgo.com/browser + Del downloadlink + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml index a120599c2b1d..2e0c1d56b4c5 100644 --- a/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-de/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + In den Einstellungen einrichten Passwort speichern @@ -168,27 +168,49 @@ Passwörter verwalten Dein Passwort wird von diesem Gerät gelöscht. - + Dein Passwort wird von diesem Gerät gelöscht. Deine Passwörter werden von diesem Gerät gelöscht. Dein Passwort wird von allen synchronisierten Geräten gelöscht. - + Dein Passwort wird von allen synchronisierten Geräten gelöscht. Deine Passwörter werden von allen synchronisierten Geräten gelöscht. Möchtest du dieses Passwort wirklich löschen? - + Möchtest du dieses %1$d Passwort wirklich löschen? Möchtest du diese %1$d Passwörter wirklich löschen? Vergewissere dich, dass du weiterhin eine Möglichkeit hast, auf dein Konto zuzugreifen. - + Vergewissere dich, dass du weiterhin eine Möglichkeit hast, auf dein Konto zuzugreifen. Vergewissere dich, dass du weiterhin eine Möglichkeit hast, auf deine Konten zuzugreifen. + Passwörter aus anderen Browsern oder Apps können mit der Desktop-Version des DuckDuckGo-Browsers importiert werden. + + Passwörter importieren + Wie man Passwörter importiert + Importiere Passwörter in der Desktop-Version des DuckDuckGo-Browsers und synchronisiere sie dann auf allen Geräten. + Desktop-Browser herunterladen + Link kopiert + Hole dir den DuckDuckGo-Browser für Mac oder Windows + Suche privat und blockiere Tracker mit dem DuckDuckGo Desktop-Browser. Besuche zum Herunterladen diesen Link auf deinem Computer.\n\n%1$s + Mit Desktop synchronisieren + Aus dem Desktop-Browser importieren: + Öffne DuckDuckGo auf Mac oder Windows + "Einstellungen > Passwörter]]>" + Passwörter importieren ... und folge den Schritten zum Importieren]]> + Sobald du sie auf deinen Computer importiert hast, kannst du die Synchronisierung auf diesem Gerät einrichten + + Lade dir die Desktop-App herunter + Hole dir DuckDuckGo für Mac oder Windows + Gehe auf deinem Computer zu: + duckduckgo.com/browser + Download-Link teilen + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml index 61fd8fbda842..3a5a4f4d9eb4 100644 --- a/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-el/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Προσαρμογή στις Ρυθμίσεις Αποθήκευση κωδικού πρόσβασης @@ -168,27 +168,49 @@ Διαχείριση κωδικών πρόσβασης Ο κωδικός πρόσβασής σας θα διαγραφεί από αυτήν τη συσκευή. - + Ο κωδικός πρόσβασής σας θα διαγραφεί από αυτήν τη συσκευή. Οι κωδικοί πρόσβασής σας θα διαγραφούν από αυτήν τη συσκευή. Ο κωδικός πρόσβασής σας θα διαγραφεί από όλες τις συγχρονισμένες συσκευές. - + Ο κωδικός πρόσβασής σας θα διαγραφεί από όλες τις συγχρονισμένες συσκευές. Οι κωδικοί πρόσβασής σας θα διαγραφούν από όλες τις συγχρονισμένες συσκευές. Θέλετε σίγουρα να διαγράψετε αυτόν τον κωδικό πρόσβασης; - + Θέλετε σίγουρα να διαγράψετε %1$d κωδικούς πρόσβασης; Θέλετε σίγουρα να διαγράψετε %1$d κωδικούς πρόσβασης; Βεβαιωθείτε ότι έχετε ακόμα τρόπο πρόσβασης στον λογαριασμό σας. - + Βεβαιωθείτε ότι έχετε ακόμα τρόπο πρόσβασης στον λογαριασμό σας. Βεβαιωθείτε ότι έχετε ακόμα τρόπο πρόσβασης στους λογαριασμούς σας. + Οι κωδικοί πρόσβασης από άλλα προγράμματα περιήγησης ή εφαρμογές μπορούν να εισαχθούν με χρήση της έκδοσης του προγράμματος περιήγησης DuckDuckGo για υπολογιστές. + + Εισαγωγή κωδικών πρόσβασης + Τρόπος εισαγωγής κωδικών πρόσβασης + Εισαγάγετε κωδικούς πρόσβασης στην έκδοση του προγράμματος περιήγησης DuckDuckGo για υπολογιστές και έπειτα συγχρονίστε τους σε όλες τις συσκευές. + Αποκτήστε το πρόγραμμα περιήγησης για υπολογιστές + Ο σύνδεσμος αντιγράφηκε + Αποκτήστε το πρόγραμμα περιήγησης DuckDuckGo για Mac ή Windows + Αναζητήστε ιδιωτικά και αποκλείστε εφαρμογές παρακολούθησης με το πρόγραμμα περιήγησης DuckDuckGo για υπολογιστές. Επισκεφτείτε αυτόν τον σύνδεσμο στον υπολογιστή σας για να πραγματοποιήσετε λήψη σήμερα.\n\n%1$s + Συγχρονισμός με υπολογιστή + Εισαγωγή από το πρόγραμμα περιήγησης για υπολογιστές: + Ανοίξτε το DuckDuckGo σε Mac ή Windows + "Ρυθμίσεις > Κωδικοί πρόσβασης]]>" + Εισαγωγή κωδικών πρόσβασης... και ακολουθήστε τα βήματα για εισαγωγή]]> + Μόλις εισαχθεί στον υπολογιστή σας, μπορείτε να ρυθμίσετε τον συγχρονισμό στη συσκευή αυτή + + Αποκτήστε την εφαρμογή για υπολογιστές + Αποκτήστε το DuckDuckGo για Mac ή Windows + Στον υπολογιστή σας, μεταβείτε στη διεύθυνση: + duckduckgo.com/browser + Κοινή χρήση συνδέσμου λήψης + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml index 058d51cdcdf1..eedaeae8950c 100644 --- a/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-es/strings-autofill-impl.xml @@ -168,27 +168,49 @@ Gestionar contraseñas Tu contraseña se borrará de este dispositivo. - + Tu contraseña se borrará de este dispositivo. Tus contraseñas se borrarán de este dispositivo. Tu contraseña se borrará en todos los dispositivos sincronizados. - + Tu contraseña se borrará en todos los dispositivos sincronizados. Tus contraseñas se borrarán en todos los dispositivos sincronizados. ¿Seguro de que quieres borrar esta contraseña? - + ¿Seguro de que quieres borrar %1$d contraseña? ¿Seguro de que quieres borrar %1$d contraseñas? Asegúrate de seguir teniendo una forma de acceder a tu cuenta. - + Asegúrate de seguir teniendo una forma de acceder a tu cuenta. Asegúrate de seguir teniendo una forma de acceder a tus cuentas. + Las contraseñas de otros navegadores o aplicaciones se pueden importar utilizando la versión de escritorio del navegador DuckDuckGo. + + Importar contraseñas + Cómo importar contraseñas + Importa contraseñas en la versión de escritorio del navegador DuckDuckGo y, a continuación, sincronízalas entre dispositivos. + Obtén el navegador de escritorio + Enlace copiado + Consigue el navegador DuckDuckGo para Mac o Windows + Busca de forma privada y bloquea los rastreadores con el navegador de escritorio DuckDuckGo. Visita este enlace en tu ordenador para descargarlo hoy mismo.\n\n%1$s + Sincronizar con el escritorio + Importar desde el navegador de escritorio: + Abre DuckDuckGo en Mac o Windows + "Ajustes > Contraseñas]]>" + Importar contraseñas... y sigue los pasos para importar]]> + Una vez importado a tu ordenador, puedes configurar la sincronización en este dispositivo + + Obtener la aplicación de escritorio + Consigue DuckDuckGo para Mac o Windows + En tu ordenador, ve a: + duckduckgo.com/browser + Compartir enlace de descarga + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml index dbd34e3cf52a..c98dfd52705d 100644 --- a/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-et/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Seadista sätetes Salvesta parool @@ -168,27 +168,49 @@ Halda paroole Teie parool kustutatakse sellest seadmest. - + Sinu parool kustutatakse sellest seadmest. Sinu paroolid kustutatakse sellest seadmest. Sinu parool kustutatakse kõigist sünkroonitud seadmetest. - + Sinu parool kustutatakse kõikidest sünkroonitud seadmetest. Sinu paroolid kustutatakse kõikidest sünkroonitud seadmetest. Kas oled kindel, et soovid selle parooli kustutada? - + Kas soovid kindlasti %1$d parooli kustutada? Kas soovid kindlasti %1$d parooli kustutada? Veendu, et sulle jääks endiselt mõni viis oma kontole pääsemiseks. - + Veendu, et sulle jääks endiselt mõni viis oma kontole pääsemiseks. Veendu, et sulle jääks endiselt mõni viis oma kontodele pääsemiseks. + Teiste brauserite või rakenduste paroole saab importida DuckDuckGo brauseri töölauaversiooni abil. + + Impordi paroolid + Kuidas importida paroole + Impordi paroolid DuckDuckGo brauseri töölauaversiooni, seejärel sünkrooni need kõigis seadmetes. + Hangi töölaua brauser + Link kopeeritud + Hangi DuckDuckGo brauser Maci või Windowsi jaoks + Otsi privaatselt ja blokeeri jälgurid DuckDuckGo töölaua brauseriga. Külasta oma arvutis seda linki, et laadida see alla juba täna.\n\n%1$s + Sünkrooni töölauaga + Impordi töölaua brauserist: + Ava DuckDuckGo Macis või Windowsis + "Seaded > Paroolid]]>" + Impordi paroolid... ja järgi samme importimiseks]]> + Kui need on arvutisse imporditud, saad seadistada selle seadme sünkroonimise + + Hangi töölaua rakendus + Hangi DuckDuckGo Maci või Windowsi jaoks + Ava oma arvutis: + duckduckgo.com/browser + Jaga allalaadimise linki + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml index 165ca5a42a47..0225aa7e65c7 100644 --- a/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-fi/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Määritä Asetuksissa Tallenna salasana @@ -168,27 +168,49 @@ Hallitse salasanoja Salasanasi poistetaan tältä laitteelta. - + Salasanasi poistetaan tältä laitteelta. Salasanasi poistetaan tältä laitteelta. Salasanasi poistetaan kaikista synkronoiduista laitteista. - + Salasanasi poistetaan kaikista synkronoiduista laitteista. Salasanasi poistetaan kaikista synkronoiduista laitteista. Haluatko varmasti poistaa tämän salasanan? - + Haluatko varmasti poistaa %1$d salasanan? Haluatko varmasti poistaa %1$d salasanaa? Varmista, että pääset yhä käyttämään tiliäsi. - + Varmista, että pääset yhä käyttämään tilejäsi. Varmista, että pääset yhä käyttämään tilejäsi. + Muiden selainten tai sovellusten salasanat voidaan tuoda DuckDuckGo-selaimen tietokoneversiolla. + + Tuo salasanat + Näin tuot salasanat + Tuo DuckDuckGo-selaimen pöytäkoneversiossa olevat salasanat ja synkronoi eri laitteiden kanssa. + Hanki pöytäkoneselain + Linkki kopioitu + Hanki DuckDuckGo-selain Macille tai Windowsille + Tee yksityisiä hakuja ja estä seuranta DuckDuckGo-selaimen tietokoneversiolla. Mene tietokoneella tähän linkkiin ja lataa selain jo tänään.\n\n%1$s + Synkronointi pöytäkoneen kanssa + Tuo pöytäkoneselaimesta: + Avaa DuckDuckGo Macissa tai Windowsissa + "Asetukset > Salasanat]]>" + Tuo salasanat… ja noudata tuontiohjeita]]> + Kun suorittanut tuonnin tietokoneellesi, voit määrittää synkronoinnin tälle laitteelle + + Hanki sovelluksen tietokoneversio + Hanki DuckDuckGo Macille tai Windowsille + Siirry tietokoneellasi kohtaan: + duckduckgo.com/browser + Jaa latauslinkki + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml index 9cc074730fdb..049ce6390e18 100644 --- a/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-fr/strings-autofill-impl.xml @@ -168,7 +168,7 @@ Gérer les mots de passe Votre mot de passe sera supprimé de cet appareil. - + Votre mot de passe sera supprimé de cet appareil. Vos mots de passe seront supprimés de cet appareil. @@ -180,15 +180,37 @@ Voulez-vous vraiment supprimer ce mot de passe ? - + Voulez-vous vraiment supprimer %1$d mot de passe ? Voulez-vous vraiment supprimer %1$d mots de passe ? Assurez-vous d\'avoir toujours un moyen d\'accéder à votre compte. - + Assurez-vous d\'avoir toujours un moyen d\'accéder à votre compte. Assurez-vous d\'avoir toujours un moyen d\'accéder à vos comptes. + Les mots de passe d\'autres navigateurs ou applications peuvent être importés à l\'aide de la version de bureau du navigateur DuckDuckGo. + + Importer les mots de passe + Comment importer des mots de passe + Importez les mots de passe dans la version de bureau du navigateur DuckDuckGo, puis synchronisez-les sur tous les appareils. + Télécharger le navigateur de bureau + Lien copié + Procurez-vous le navigateur DuckDuckGo pour Mac ou Windows + Faites une recherche privée et bloquez les traqueurs avec le navigateur de bureau DuckDuckGo. Cliquez sur ce lien sur votre ordinateur pour le télécharger dès aujourd\'hui.\n\n%1$s + Synchronisation avec le navigateur de bureau + Importer depuis le navigateur de bureau : + Ouvrez DuckDuckGo sur Mac ou Windows + "Paramètres > Mots de passe]]>" + Importer les mots de passe… et suivez les étapes pour l\'importation]]> + Une fois l\'importation effectuée sur votre ordinateur, vous pouvez configurer la synchronisation sur cet appareil + + Obtenir l\'application de bureau + Procurez-vous DuckDuckGo pour Mac ou Windows + Sur votre ordinateur, accédez à : + duckduckgo.com/browser + Partager le lien de téléchargement + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml index 6a98e17a824b..3141b21f7c57 100644 --- a/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-hr/strings-autofill-impl.xml @@ -188,7 +188,7 @@ Sigurno želiš izbrisati ovu lozinku? - + Sigurno želiš izbrisati %1$d lozinku? Sigurno želiš izbrisati %1$d lozinke? Sigurno želiš izbrisati %1$d lozinki? @@ -203,4 +203,26 @@ Uvjeri se da i dalje imaš način pristupa svojim računima. + Lozinke iz drugih preglednika ili aplikacija mogu se uvesti pomoću desktop verzije preglednika DuckDuckGo. + + Uvezi lozinke + Kako uvesti lozinke + Uvezi lozinke u stolnu verziju preglednika DuckDuckGo, a zatim sinkroniziraj preko uređaja. + Nabavi preglednik za PC + Poveznica je kopirana + Nabavi DuckDuckGo preglednik za Mac ili Windows + Privatno pretražuj i blokiraj alate za praćenje pomoću stolne verzije preglednika DuckDuckGo. Za preuzimanje, posjeti ovu poveznicu na svom računalu.\n\n%1$s + Sinkronizacija s preglednikom na PC-u + Uvoz iz preglednika PC-a: + Otvori DuckDuckGo na Macu ili u Windowsima + "Postavke > Lozinke]]>" + Uvezi lozinke... i slijedi korake za uvoz]]> + Nakon uvoza na svoje računalo, možeš postaviti sinkronizaciju na ovom uređaju + + Preuzmi aplikaciju za stolna računala + Nabavi DuckDuckGo za Mac ili Windows + Na računalu idi na: + duckduckgo.com/browser + Podijeli poveznicu za preuzimanje + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml index 6bba45b3cd97..dcdfc47b6282 100644 --- a/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-hu/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Beállítás a Beállításokban Jelszó mentése @@ -168,27 +168,49 @@ Jelszavak kezelése A jelszó törölve lesz erről az eszközről. - + A jelszavad törölve lesz erről az eszközről. A jelszavaid törölve lesznek erről az eszközről. A jelszó az összes szinkronizált eszközről törölve lesz. - + A jelszavad törölve lesz minden szinkronizált eszközről. A jelszavaid törölve lesznek minden szinkronizált eszközről. Biztosan törlöd ezt a jelszót? - + Biztosan törölni szeretnél %1$d jelszót? Biztosan törölni szeretnél %1$d jelszót? Győződj meg róla, hogy továbbra is hozzáférsz a fiókodhoz. - + Győződj meg róla, hogy továbbra is hozzáférsz a fiókodhoz. Győződj meg róla, hogy továbbra is hozzáférsz a fiókjaidhoz. + Más böngészőkből vagy alkalmazásokból származó jelszavak a DuckDuckGo böngésző asztali verziójával importálhatók. + + Jelszavak importálása + Jelszavak importálásának módja + Importálj jelszavakat a DuckDuckGo böngésző asztali verziójába, majd szinkronizáld őket az eszközök között. + Asztali böngésző letöltése + Link másolva + Mac vagy Windows verziójú DuckDuckGo böngésző letöltése + Keress privát módon, és blokkold a nyomkövetőket a DuckDuckGo asztali böngészőjével. A letöltéshez nyisd meg ezt a linket a számítógépen.\n\n%1$s + Szinkronizálás asztali böngészővel + Importálás az asztali böngészőből: + Nyisd meg Mac vagy Windows rendszeren a DuckDuckGo böngészőt + "Beállítások > Jelszavak oldalra]]>" + Jelszavak importálása… lehetőséget, és kövesd az importáláshoz szükséges lépéseket]]> + A számítógépre való importálás után beállíthatod a szinkronizálást ezen az eszközön + + Asztali alkalmazás letöltése + Mac vagy Windows verziójú DuckDuckGo letöltése + A számítógépeden lépj a következő lehetőségre: + duckduckgo.com/browser + Letöltési link megosztása + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml index 7c722d68ddbb..119af21db473 100644 --- a/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-it/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Configura in Impostazioni Salva password @@ -168,27 +168,49 @@ Gestisci password La password sarà eliminata da questo dispositivo. - + La password sarà eliminata da questo dispositivo. Le password saranno eliminate da questo dispositivo. La password verrà eliminata da tutti i dispositivi sincronizzati. - + La password sarà eliminata da tutti i dispositivi sincronizzati. Le password saranno eliminate da tutti i dispositivi sincronizzati. Eliminare questa password? - + Eliminare %1$d password? Eliminare %1$d password? Assicurati di avere ancora la possibilità di accedere al tuo account. - + Assicurati di avere ancora la possibilità di accedere al tuo account. Assicurati di avere ancora la possibilità di accedere ai tuoi account. + Le password di altri browser o app possono essere importate utilizzando la versione desktop del browser DuckDuckGo. + + Importa password + Come importare le password + Importa le password nella versione desktop del browser DuckDuckGo, quindi sincronizza su tutti i dispositivi. + Scarica il browser per desktop + Link copiato + Scarica il browser DuckDuckGo per Mac o Windows + Cerca privatamente e blocca i sistemi di tracciamento con il browser per desktop DuckDuckGo. Visita questo link dal computer per scaricarlo oggi stesso.\n\n%1$s + Sincronizzazione con il desktop + Importa dal browser desktop: + Apri DuckDuckGo su Mac o Windows + "Impostazioni > Password]]>" + Importa password… e segui i passaggi per importare]]> + Dopo l\'importazione sul tuo computer, puoi impostare la sincronizzazione su questo dispositivo + + Scarica l\'app per desktop + Scarica DuckDuckGo per Mac o Windows + Sul tuo computer, visita: + duckduckgo.com/browser + Condividi link per il download + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml index 54d172ead0f9..bb307f785712 100644 --- a/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-lt/strings-autofill-impl.xml @@ -188,7 +188,7 @@ Ar tikrai norite ištrinti šį slaptažodį? - + Ar tikrai norite ištrinti %1$d slaptažodį? Ar tikrai norite ištrinti %1$d slaptažodžius? Ar tikrai norite ištrinti %1$d slaptažodžio? @@ -203,4 +203,26 @@ Įsitikinkite, kad vis dar turite būdą, kaip pasiekti paskyras. + Slaptažodžius iš kitų naršyklių ar programų galima importuoti naudojant „DuckDuckGo“ naršyklės versiją kompiuteriui. + + Importuoti slaptažodžius + Kaip importuoti slaptažodžius + Importuokite slaptažodžius „DuckDuckGo“ naršyklės versijoje kompiuteriui, tada sinchronizuokite juos visuose įrenginiuose. + Gaukite kompiuterio naršyklę + Nuoroda nukopijuota + Gaukite „DuckDuckGo“ naršyklę, skirtą „Mac“ arba „Windows“ + Ieškokite privačiai ir blokuokite sekimo priemones naudodami „DuckDuckGo“ naršyklę kompiuteriui. Spustelėkite šią nuorodą kompiuteryje, kad atsisiųstumėte šiandien.\n\n%1$s + Sinchronizavimas su kompiuteriu + Importuoti iš kompiuterio naršyklės: + Atidarykite „DuckDuckGo“ sistemoje „Mac“ arba „Windows“ + "Nustatymai > Slaptažodžiai]]>" + Importuoti slaptažodžius... ir atlikite veiksmus, kad importuotumėte]]> + Kai importuosite į savo kompiuterį, galite nustatyti sinchronizavimą šiame įrenginyje + + Gauti kompiuterio programą + Gaukite „DuckDuckGo“, skirtą „Mac“ arba „Windows“ + Kompiuteryje eikite į: + duckduckgo.com/browser + Bendrinti atsisiuntimo nuorodą + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml index ce77e174e71b..9f3295b32eaf 100644 --- a/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-lv/strings-autofill-impl.xml @@ -184,7 +184,7 @@ Vai tiešām vēlies dzēst šo paroli? - + Vai tiešām vēlies dzēst %1$d paroles? Vai tiešām vēlies dzēst %1$d paroli? Vai tiešām vēlies dzēst %1$d paroles? @@ -197,4 +197,26 @@ Pārliecinies, vai joprojām varēsi piekļūt savam kontam. + Paroles no citiem pārlūkiem vai lietotnēm var importēt, izmantojot DuckDuckGo pārlūka galddatora versiju. + + Importēt paroles + Kā importēt paroles + Importē paroles DuckDuckGo pārlūka galddatora versijā un pēc tam sinhronizē tās visās ierīcēs. + Iegūsti galddatora pārlūku + Saite nokopēta + Iegūsti DuckDuckGo pārlūku Mac vai Windows datoram + Meklē privāti un bloķē izsekotājus, izmantojot DuckDuckGo pārlūku galddatoram. Apmeklē šo saiti savā datorā, lai lejupielādētu jau šodien.\n\n%1$s + Sinhronizēt ar galddatoru + Importēšana no galddatora pārlūka: + Atver DuckDuckGo Mac vai Windows datorā + "Iestatījumi > Paroles]]>" + Importēt paroles un izpildi darbības, lai importētu]]> + Kad esi importējis paroles datorā, vari iestatīt sinhronizāciju šajā ierīcē + + Iegūt datora programmu + Iegūsti DuckDuckGo Mac vai Windows datoram + Savā datorā dodies uz: + duckduckgo.com/browser + Kopīgot lejupielādes saiti + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml index 0d7c224c9c77..981b2dc2ffd7 100644 --- a/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-nb/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Konfigurer i Innstillinger Lagre passordet @@ -168,27 +168,49 @@ Administrer passord Passordet ditt blir slettet fra denne enheten. - + Passordet ditt blir slettet fra denne enheten. Passordene dine blir slettet fra denne enheten. Passordet ditt blir slettet fra alle synkroniserte enheter. - + Passordet ditt blir slettet fra alle synkroniserte enheter. Passordene dine blir slettet fra alle synkroniserte enheter. Er du sikker på at du vil slette dette passordet? - + Er du sikker på at du vil slette %1$d passord? Er du sikker på at du vil slette %1$d passord? Sørg for at du fortsatt har tilgang til kontoen din. - + Sørg for at du fortsatt har tilgang til kontoen din. Sørg for at du fortsatt har tilgang til kontoene dine. + Passord fra andre nettlesere eller apper kan importeres med skrivebordsversjonen av DuckDuckGo-nettleseren. + + Importer passord + Slik importerer du passord + Importer passord i datamaskinversjonen av DuckDuckGo-nettleseren, og så kan du synkronisere mellom enheter. + Skaff deg nettleseren for datamaskin + Lenken er kopiert + Skaff deg DuckDuckGo-nettleseren for Mac eller Windows + Søk privat og blokker sporere med DuckDuckGo-nettleseren for datamaskin. Gå til denne lenken på datamaskinen for å laste ned i dag.\n\n%1$s + Synkroniser med datamaskinen + Importer fra datamaskinnettleseren: + Åpne DuckDuckGo på Mac eller Windows + "Innstillinger > Passord]]>" + Importer passord … og følg instruksjonene for å importere]]> + Når de er importert til datamaskinen, kan du sette opp synkronisering på denne enheten + + Hent skrivebordsappen + Skaff deg DuckDuckGo for Mac eller Windows + På datamaskinen går du til: + duckduckgo.com/browser + Del nedlastingslenke + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml index 376d8a60e610..855b6193f39d 100644 --- a/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-nl/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Instellen in Instellingen Wachtwoord opslaan @@ -168,27 +168,49 @@ Wachtwoorden beheren Je wachtwoord wordt van dit apparaat verwijderd. - + Je wachtwoord wordt van dit apparaat verwijderd. Je wachtwoorden worden van dit apparaat verwijderd. Je wachtwoord wordt verwijderd van alle gesynchroniseerde apparaten. - + Je wachtwoord wordt verwijderd van alle gesynchroniseerde apparaten. Je wachtwoorden worden verwijderd van alle gesynchroniseerde apparaten. Weet je zeker dat je dit wachtwoord wilt verwijderen? - + Weet je zeker dat je %1$d wachtwoord wilt verwijderen? Weet je zeker dat je %1$d wachtwoorden wilt verwijderen? Zorg ervoor dat je nog steeds toegang hebt tot je account. - + Zorg ervoor dat je nog steeds toegang hebt tot je account. Zorg ervoor dat je nog steeds toegang hebt tot je accounts. + Wachtwoorden van andere browsers of apps kunnen worden geïmporteerd met de desktopversie van de DuckDuckGo-browser. + + Wachtwoorden importeren + Wachtwoorden importeren + Importeer wachtwoorden in de desktopversie van de DuckDuckGo-browser en synchroniseer ze vervolgens tussen apparaten. + Download de desktopbrowser + Link gekopieerd + DuckDuckGo-browser downloaden voor Mac of Windows + Zoek privé en blokkeer trackers met de DuckDuckGo-desktopbrowser. Bezoek deze link op je computer en download de browser vandaag nog.\n\n%1$s + Synchroniseren met de desktop + Importeren vanuit de desktopbrowser: + Open DuckDuckGo op Mac of Windows + "Instellingen > wachtwoorden]]>" + Wachtwoorden importeren... en volg de stappen om te importeren]]> + Na het importeren op uw computer, kunt u synchronisatie instellen op dit apparaat + + Desktop-app downloaden + DuckDuckGo downloaden voor Mac of Windows + Ga op je computer naar: + duckduckgo.com/browser + Downloadlink delen + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml index 10b2abfa157e..609c0fbcb302 100644 --- a/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-pl/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Dostosuj ustawienia Zapisz hasło @@ -172,7 +172,7 @@ Zarządzaj hasłami Twoje hasło zostanie usunięte z tego urządzenia. - + Twoje hasło zostanie usunięte z tego urządzenia. Twoje hasła zostaną usunięte z tego urządzenia. Twoje hasła zostaną usunięte z tego urządzenia. @@ -180,7 +180,7 @@ Twoje hasło zostanie usunięte ze wszystkich zsynchronizowanych urządzeń. - + Twoje hasło zostanie usunięte ze wszystkich zsynchronizowanych urządzeń. Twoje hasła zostaną usunięte ze wszystkich zsynchronizowanych urządzeń. Twoje hasła zostaną usunięte ze wszystkich zsynchronizowanych urządzeń. @@ -188,7 +188,7 @@ Czy na pewno chcesz usunąć to hasło? - + Czy na pewno chcesz usunąć %1$d hasło? Czy na pewno chcesz usunąć %1$d hasła? Czy na pewno chcesz usunąć %1$d haseł? @@ -196,11 +196,33 @@ Upewnij się, że nadal masz możliwość dostępu do swojego konta. - + Upewnij się, że nadal masz możliwość dostępu do swojego konta. Upewnij się, że nadal masz możliwość dostępu do swoich kont. Upewnij się, że nadal masz możliwość dostępu do swoich kont. Upewnij się, że nadal masz możliwość dostępu do swoich kont. + Hasła z innych przeglądarek lub aplikacji można zaimportować za pomocą komputerowej wersji przeglądarki DuckDuckGo. + + Importuj hasła + Jak zaimportować hasła + Zaimportuj hasła w komputerowej wersji przeglądarki DuckDuckGo, a następnie zsynchronizuj je na różnych urządzeniach. + Pobierz przeglądarkę komputerową + Skopiowano łącze + Pobierz przeglądarkę DuckDuckGo dla komputerów Mac lub systemu Windows + Wyszukuj prywatnie i blokuj skrypty śledzące za pomocą przeglądarki komputerowej DuckDuckGo. Odwiedź ten link na komputerze, aby pobrać ją już dziś.\n\n%1$s + Synchronizuj z komputerem + Zaimportuj z przeglądarki komputerowej: + Otwórz DuckDuckGo na komputerze Mac lub w systemie Windows + "Ustawienia > Hasła]]>" + Importuj hasła... i postępuj zgodnie z instrukcjami importu]]> + Po zaimportowaniu na komputer możesz skonfigurować synchronizację na urządzeniu + + Pobierz aplikację komputerową + Pobierz DuckDuckGo dla komputerów Mac lub systemu Windows + Na komputerze przejdź do: + duckduckgo.com/browser + Udostępnij link pobierania + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml index 668ba711d916..1535b65b25b9 100644 --- a/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-pt/strings-autofill-impl.xml @@ -180,7 +180,7 @@ Tens a certeza de que pretendes eliminar esta palavra-passe? - + Tens a certeza de que pretendes eliminar %1$d palavra-passe? Tens a certeza de que pretendes eliminar %1$d palavras-passe? @@ -191,4 +191,26 @@ Confirma que ainda tens uma forma de aceder às contas. + As palavras-passe de outros navegadores ou aplicações podem ser importadas com a versão para computadores do navegador DuckDuckGo. + + Importar palavras-passe + Como importar palavras-passe + Importa palavras-passe na versão para computadores do navegador DuckDuckGo e sincroniza-as entre dispositivos. + Obter navegador para computador + Link copiado + Obtém o navegador DuckDuckGo para Mac ou Windows + Pesquisa em privado e bloqueia rastreadores com o navegador para computadores DuckDuckGo. Visita este link no teu computador para transferires hoje.\n\n%1$s + Sincronizar com computador + Importar do navegador para computadores: + Abre o DuckDuckGo no Mac ou Windows + "Definições > Palavras-passe]]>" + Importar palavras-passe… e segue os passos para importar]]> + Depois de importares as palavras-passe, podes configurar a sincronização neste dispositivo + + Obter aplicação para computador + Obtém o DuckDuckGo para Mac ou Windows + No teu computador, acede a: + duckduckgo.com/browser + Partilhar link de transferência + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml index 98d82ea95ac5..b45e821adc29 100644 --- a/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-ro/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Configurează în Setări Salvează parola @@ -170,31 +170,53 @@ Gestionează parolele Parola ta va fi ștearsă de pe acest dispozitiv. - + Parola ta va fi ștearsă de pe acest dispozitiv. Parolele tale vor fi șterse de pe acest dispozitiv. Parolele tale vor fi șterse de pe acest dispozitiv. Parola ta va fi ștearsă de pe toate dispozitivele sincronizate. - + Parola ta va fi ștearsă de pe toate dispozitivele sincronizate. Parolele tale vor fi șterse de pe toate dispozitivele sincronizate. Parolele tale vor fi șterse de pe toate dispozitivele sincronizate. Sigur dorești să ștergi această parolă? - + Sigur dorești să ștergi %1$d parolă? Sigur dorești să ștergi %1$d parole? Sigur dorești să ștergi această %1$d de parole? Asigură-te că ai în continuare posibilitatea de a-ți accesa contul. - + Asigură-te că ai în continuare posibilitatea de a-ți accesa contul. Asigură-te că ai în continuare posibilitatea de a-ți accesa conturile. Asigură-te că ai în continuare posibilitatea de a-ți accesa conturile. + Parolele din alte browsere sau aplicații pot fi importate folosind versiunea pentru desktop a browserului DuckDuckGo. + + Importă parolele + Cum să imporți parolele + Importă parolele în versiunea desktop a browserului DuckDuckGo, apoi sincronizează-le între dispozitive. + Obține browserul pentru desktop + Link copiat + Obține browserul DuckDuckGo pentru Mac sau Windows + Caută în mod privat și blochează tehnologiile de urmărire cu browserul pentru desktop DuckDuckGo. Accesează acest link pe computerul tău pentru a descărca astăzi.\n\n%1$s + Sincronizare cu computerul desktop + Importă din browserul desktop: + Deschide DuckDuckGo pe Mac sau Windows + "Setări > Parole]]>" + Importă parole... și urmează pașii pentru a importa]]> + Odată importate pe computer, poți configura sincronizarea pe acest dispozitiv + + Obține aplicația pentru desktop + Obține DuckDuckGo pentru Mac sau Windows + De pe computerul tău, accesează: + duckduckgo.com/browser + Trimite linkul de descărcare + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml index 9bd9213a61e1..1efb72d3540d 100644 --- a/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-ru/strings-autofill-impl.xml @@ -188,7 +188,7 @@ Вы точно хотите удалить этот пароль? - + Вы точно хотите удалить %1$d пароль? Вы точно хотите удалить %1$d пароля? Вы точно хотите удалить %1$d паролей? @@ -203,4 +203,26 @@ Обязательно убедитесь, что вы можете войти в свои учетные записи другим способом. + Пароли из других браузеров и приложений можно импортировать с помощью настольной версии DuckDuckGo. + + Импорт паролей + Как импортировать пароли + Импортируйте пароли в настольную версию браузера DuckDuckGo, а затем синхронизируйте их между устройствами. + Скачать настольный браузер + Ссылка скопирована + Браузер DuckDuckGo для Mac и Windows + Браузер DuckDuckGo для настольных компьютеров — это конфиденциальный поиск плюс блокировка трекеров. Скачивайте по ссылке.\n\n%1$s + Синхронизировать с компьютером + Импорт из настольного браузера: + Запустите DuckDuckGo на Mac или Windows + "Настройки > Пароли»]]>" + Импортировать пароли...» и следуйте дальнейшим инструкциям.]]> + По окончании импортирования на компьютер вы сможете настроить синхронизацию на данном устройстве. + + Наше настольное приложение + DuckDuckGo для Mac и Windows + На компьютере откройте ссылку: + duckduckgo.com/browser + Поделиться ссылкой для загрузки + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml index c8bdd217167b..66b5517c5cf2 100644 --- a/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-sk/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Nastaviť v Nastaveniach Uložiť heslo @@ -172,7 +172,7 @@ Spravovať heslá Vaše heslo bude z tohto zariadenia odstránené. - + Vaše heslo bude z tohto zariadenia odstránené. Vaše heslá budú z tohto zariadenia odstránené. Vaše heslá budú z tohto zariadenia odstránené. @@ -180,7 +180,7 @@ Vaše heslo sa odstráni zo všetkých synchronizovaných zariadení. - + Vaše heslo sa odstráni zo všetkých synchronizovaných zariadení. Vaše heslá sa odstránia zo všetkých synchronizovaných zariadení. Vaše heslá sa odstránia zo všetkých synchronizovaných zariadení. @@ -188,7 +188,7 @@ Naozaj chcete odstrániť toto heslo? - + Naozaj chcete odstrániť %1$d heslo? Naozaj chcete odstrániť %1$d heslá? Naozaj chcete odstrániť %1$d hesla? @@ -196,11 +196,33 @@ Zabezpečte si prístup k svojmu účtu. - + Zabezpečte si prístup k svojmu účtu. Zabezpečte si prístup k svojim účtom. Zabezpečte si prístup k svojim účtom. Zabezpečte si prístup k svojim účtom. + Heslá z iných prehliadačov alebo aplikácií možno importovať pomocou verzie prehliadača DuckDuckGo pre počítače. + + Importovať heslá + Ako importovať heslá + Heslá môžete importovať do počítačovej verzie prehliadača DuckDuckGo a potom ich synchronizovať medzi zariadeniami. + Získajte prehliadač pre desktop PC + Odkaz bol skopírovaný + Získajte prehliadač DuckDuckGo pre Mac alebo Windows + Vyhľadávajte súkromne a blokujte sledovacie zariadenia pomocou prehliadača DuckDuckGo pre počítače. Navštívte tento odkaz vo svojom počítači a stiahnite si ho ešte dnes.\n\n%1$s + Synchronizácia s počítačom + Importovať z prehliadača na počítači: + Otvoriť DuckDuckGo na Macu alebo v systéme Windows + "časti Nastavenia > Heslá]]>" + Importovať heslá... a pre importovanie postupujte podľa krokov]]> + Po importovaní do počítača môžete nastaviť synchronizáciu na tomto zariadení + + Získajte aplikáciu pre počítače + Získajte DuckDuckGo pre Mac alebo Windows + V počítači prejdite na: + duckduckgo.com/browser + Zdieľať odkaz na stiahnutie + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml index 4527aa108a5e..5b93eff28579 100644 --- a/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-sl/strings-autofill-impl.xml @@ -188,7 +188,7 @@ Ali ste prepričani, da želite izbrisati to geslo? - + Ali ste prepričani, da želite izbrisati %1$d geslo? Ali ste prepričani, da želite izbrisati %1$d gesli? Ali ste prepričani, da želite izbrisati %1$d gesla? @@ -203,4 +203,26 @@ Prepričajte se, da imate še vedno možnost dostopa do svojih računov. + Gesla iz drugih brskalnikov ali aplikacij lahko uvozite z namizno različico brskalnika DuckDuckGo. + + Uvozi gesla + Kako uvoziti gesla + V namizni različici brskalnika DuckDuckGo uvozite gesla in jih nato sinhronizirajte med napravami. + Namesti namizni brskalnik + Povezava je kopirana + Prenesite brskalnik DuckDuckGo za računalnike Mac ali Windows + Brskajte zasebno in blokirajte sledilnike z namiznim brskalnikom DuckDuckGo. Za prenos obiščite to povezavo v računalniku.\n\n%1$s + Sinhronizacija z namizjem + Uvoz iz namiznega brskalnika: + Odprite DuckDuckGo v sistemu Mac ali Windows + "Nastavitve > Gesla]]>" + Uvozi gesla ... in upoštevajte korake za uvoz]]> + Ko jih uvozite v računalnik, lahko nastavite sinhronizacijo v tej napravi + + Pridobite namizno aplikacijo + Prenesite DuckDuckGo za računalnike Mac ali Windows + V računalniku odprite: + duckduckgo.com/browser + Deli povezavo za prenos + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml index 2496d1d6bca4..6c630618a24a 100644 --- a/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-sv/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Konfigurera i Inställningar Spara lösenord @@ -168,27 +168,49 @@ Hantera lösenord Ditt lösenord kommer att raderas från denna enhet. - + Ditt lösenord kommer att raderas från denna enhet. Dina lösenord kommer att raderas från denna enhet. Ditt lösenord kommer att raderas från alla synkroniserade enheter. - + Ditt lösenord kommer att raderas från alla synkroniserade enheter. Dina lösenord kommer att raderas från alla synkroniserade enheter. Är du säker på att du vill radera detta lösenord? - + Är du säker på att du vill radera %1$d lösenord? Är du säker på att du vill radera %1$d lösenord? Se till att du har kvar ett sätt att komma åt ditt konto. - + Se till att du har kvar ett sätt att komma åt ditt konto. Se till att du har kvar ett sätt att komma åt dina konton. + Lösenord från andra webbläsare eller appar kan importeras med hjälp av datorversionen av DuckDuckGo-webbläsaren. + + Importera lösenord + Så här importerar du lösenord + Importera lösenord i versionen av DuckDuckGo-webbläsaren för dator och synkronisera sedan mellan enheter. + Hämta webbläsare för dator + Länken har kopierats + Hämta DuckDuckGo-webbläsaren för Mac eller Windows + Sök privat och blockera spårare med DuckDuckGo-webbläsaren för datorn. Gå till denna länk på din dator för att ladda ner den idag.\n\n%1$s + Synkronisera med dator + Importera från webbläsare för dator: + Öppna DuckDuckGo på Mac eller Windows + "Inställningar > Lösenord]]>" + Importera lösenord... och följ anvisningarna för att importera]]> + När du har importerat till din dator kan du ställa in synkronisering på denna enhet + + Hämta app för dator + Hämta DuckDuckGo för Mac eller Windows + På din dator går du till: + duckduckgo.com/browser + Dela nedladdningslänk + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml index d8ab55eb04ba..446123b65783 100644 --- a/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values-tr/strings-autofill-impl.xml @@ -16,7 +16,7 @@ --> - + Ayarlarda Ayarlama Şifreyi Kaydet @@ -168,27 +168,49 @@ Şifreleri Yönet Şifreniz bu cihazdan silinecektir. - + Şifreniz bu cihazdan silinecek. Şifreleriniz bu cihazdan silinecek. Şifreniz senkronize edilen tüm cihazlardan silinecek. - + Şifreniz senkronize edilen tüm cihazlardan silinecek. Şifreleriniz senkronize edilen tüm cihazlardan silinecek. Bu şifreyi silmek istediğinizden emin misiniz? - + Bu %1$d şifreyi silmek istediğinizden emin misiniz? Bu %1$d şifreyi silmek istediğinizden emin misiniz? Hesabınıza başka bir şekilde erişebilecek olduğunuzdan emin olun. - + Hesabınıza başka bir şekilde erişebileceğinizden emin olun. Hesaplarınıza başka bir şekilde erişebileceğinizden emin olun. + Diğer tarayıcılardan veya uygulamalardan şifreler, DuckDuckGo tarayıcısının masaüstü sürümü kullanılarak içe aktarılabilir. + + Şifreleri İçe Aktar + Şifreler Nasıl Aktarılır + DuckDuckGo tarayıcısının masaüstü sürümünde şifreleri içe aktarın, ardından cihazlar arasında senkronize edin. + Masaüstü Tarayıcısını Edinin + Bağlantı kopyalandı + Mac veya Windows için DuckDuckGo Tarayıcısını edinin + DuckDuckGo masaüstü tarayıcısı ile gizli arama yapın ve izleyicileri engelleyin. Bugün indirmek için bilgisayarınızda bu bağlantıyı ziyaret edin.\n\n%1$s + Masaüstü ile Senkronize Et + Masaüstü tarayıcısından içe aktarın: + DuckDuckGo\'yu Mac veya Windows\'ta açın + "Ayarlar > Şifreler'e gidin]]>" + Şifreleri İçe Aktar… öğesini seçin ve içe aktarma adımlarını izleyin]]> + Bilgisayarınıza aktarıldıktan sonra bu cihazda senkronizasyonu ayarlayabilirsiniz + + Masaüstü Uygulamasını İndirin + Mac veya Windows için DuckDuckGo\'yu edinin + Bilgisayarınızda şu adrese gidin: + duckduckgo.com/browser + İndirme Bağlantısını Paylaş + \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values/donottranslate.xml b/autofill/autofill-impl/src/main/res/values/donottranslate.xml index 1e6fa06ec0c5..def6aa352279 100644 --- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml +++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml @@ -15,25 +15,5 @@ --> - Passwords from other browsers or apps can be imported using the desktop version of the DuckDuckGo browser. - Import Passwords - How To Import Passwords - Import passwords in the desktop version of the DuckDuckGo browser, then sync across devices. - Get Desktop Browser - Link copied - Get DuckDuckGo Browser for Mac or Windows - Search privately and block trackers with the DuckDuckGo desktop browser. Visit this link on your computer to download today.\n\n%1$s - Sync With Desktop - Import from the desktop browser: - Open DuckDuckGo on Mac or Windows - "Settings > Passwords]]>" - Select Import Passwords]]>… and follow the steps to import - Once imported on your computer you can set up sync on this device - - Get Desktop App - Get DuckDuckGo for Mac or Windows - On your computer, go to: - duckduckgo.com/browser - Share Download Link \ No newline at end of file diff --git a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml index d8708583c9d4..ac1447766ef8 100644 --- a/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml +++ b/autofill/autofill-impl/src/main/res/values/strings-autofill-impl.xml @@ -16,7 +16,7 @@ - + Set up in Settings Save Password @@ -168,23 +168,45 @@ Manage Passwords Your password will be deleted from this device. - + Your passwords will be deleted from this device. Your password will be deleted from all synced devices. - + Your passwords will be deleted from all synced devices. Are you sure you want to delete this password? - + Are you sure you want to delete %1$d passwords? Make sure you still have a way to access your account. - + Make sure you still have a way to access your accounts. + Passwords from other browsers or apps can be imported using the desktop version of the DuckDuckGo browser. + + Import Passwords + How To Import Passwords + Import passwords in the desktop version of the DuckDuckGo browser, then sync across devices. + Get Desktop Browser + Link copied + Get DuckDuckGo Browser for Mac or Windows + Search privately and block trackers with the DuckDuckGo desktop browser. Visit this link on your computer to download today.\n\n%1$s + Sync With Desktop + Import from the desktop browser: + Open DuckDuckGo on Mac or Windows + "Settings > Passwords]]>" + Select Import Passwords]]>… and follow the steps to import + Once imported on your computer you can set up sync on this device + + Get Desktop App + Get DuckDuckGo for Mac or Windows + On your computer, go to: + duckduckgo.com/browser + Share Download Link + \ No newline at end of file From b497807ceaf131a5fc531a2aaae5f2578230594a Mon Sep 17 00:00:00 2001 From: Karl Dimla Date: Mon, 15 Jul 2024 15:38:43 +0200 Subject: [PATCH 06/20] Improve VPN failure recovery (#4737) Task/Issue URL: https://app.asana.com/0/488551667048375/1207679844545694/f ### Description See attached task description ### Steps to test this PR https://app.asana.com/0/488551667048375/1207756442345818/f --- .../impl/failure/FailureRecoveryHandler.kt | 40 ++++++--- .../impl/pixels/WireguardHandshakeMonitor.kt | 12 ++- .../failure/FailureRecoveryHandlerTest.kt | 88 +++++++++++-------- 3 files changed, 83 insertions(+), 57 deletions(-) diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/failure/FailureRecoveryHandler.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/failure/FailureRecoveryHandler.kt index b0c6495494f2..020d0a8e525b 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/failure/FailureRecoveryHandler.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/failure/FailureRecoveryHandler.kt @@ -16,6 +16,8 @@ package com.duckduckgo.networkprotection.impl.failure +import com.duckduckgo.common.utils.ConflatedJob +import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.VpnScope import com.duckduckgo.mobile.android.vpn.VpnFeaturesRegistry import com.duckduckgo.networkprotection.impl.CurrentTimeProvider @@ -26,9 +28,12 @@ import com.duckduckgo.networkprotection.impl.configuration.asServerDetails import com.duckduckgo.networkprotection.impl.pixels.NetworkProtectionPixels import com.duckduckgo.networkprotection.impl.pixels.WireguardHandshakeMonitor import com.squareup.anvil.annotations.ContributesMultibinding +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import logcat.LogPriority import logcat.asLog import logcat.logcat @@ -40,22 +45,28 @@ class FailureRecoveryHandler @Inject constructor( private val wgTunnelConfig: WgTunnelConfig, private val currentTimeProvider: CurrentTimeProvider, private val networkProtectionPixels: NetworkProtectionPixels, + private val dispatcherProvider: DispatcherProvider, ) : WireguardHandshakeMonitor.Listener { - private var recoveryCompleted = false - private var recoveryInProgress = false - override suspend fun onTunnelFailure(lastHandshakeEpocSeconds: Long) { + private var failureRecoveryInProgress = AtomicBoolean(false) + private val job = ConflatedJob() + + override suspend fun onTunnelFailure( + coroutineScope: CoroutineScope, + lastHandshakeEpocSeconds: Long, + ) { val nowSeconds = currentTimeProvider.getTimeInEpochSeconds() val diff = nowSeconds - lastHandshakeEpocSeconds - if (diff.seconds.inWholeMinutes >= FAILURE_RECOVERY_THRESHOLD_MINUTES && !recoveryInProgress) { + if (diff.seconds.inWholeMinutes >= FAILURE_RECOVERY_THRESHOLD_MINUTES && !failureRecoveryInProgress.get()) { logcat { "Failure recovery: starting recovery" } - recoveryInProgress = true - recoveryCompleted = false - incrementalPeriodicChecks { - recoveryCompleted = attemptRecovery().isSuccess + failureRecoveryInProgress.set(true) + job += coroutineScope.launch(dispatcherProvider.io()) { + incrementalPeriodicChecks { + attemptRecovery() + } } } else { - if (recoveryInProgress) { + if (failureRecoveryInProgress.get()) { logcat { "Failure recovery: Recovery already in progress. Do nothing" } } else { logcat { "Failure recovery: time since lastHandshakeEpocSeconds is not within failure recovery threshold" } @@ -63,14 +74,15 @@ class FailureRecoveryHandler @Inject constructor( } } - override suspend fun onTunnelFailureRecovered() { + override suspend fun onTunnelFailureRecovered(coroutineScope: CoroutineScope) { + logcat { "Failure recovery: tunnel recovered, cancelling recovery" } + job.cancel() wgTunnel.markTunnelHealthy() - recoveryCompleted = true - recoveryInProgress = false + failureRecoveryInProgress.set(false) } private suspend fun incrementalPeriodicChecks( - times: Int = 5, + times: Int = 140, // Adding a cap of around 12 hours - ideally a device should recover around that time. initialDelay: Long = 30_000, // 30 seconds maxDelay: Long = 300_000, // 5 minutes factor: Double = 2.0, @@ -79,7 +91,7 @@ class FailureRecoveryHandler @Inject constructor( var currentDelay = initialDelay repeat(times) { try { - if (!recoveryCompleted) { + if (failureRecoveryInProgress.get()) { block() } else { return@incrementalPeriodicChecks diff --git a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/pixels/WireguardHandshakeMonitor.kt b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/pixels/WireguardHandshakeMonitor.kt index aba147daf672..e5cd76658c3e 100644 --- a/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/pixels/WireguardHandshakeMonitor.kt +++ b/network-protection/network-protection-impl/src/main/java/com/duckduckgo/networkprotection/impl/pixels/WireguardHandshakeMonitor.kt @@ -57,8 +57,12 @@ class WireguardHandshakeMonitor @Inject constructor( ) : VpnServiceCallbacks { interface Listener { - suspend fun onTunnelFailure(lastHandshakeEpocSeconds: Long) - suspend fun onTunnelFailureRecovered() + suspend fun onTunnelFailure( + coroutineScope: CoroutineScope, + lastHandshakeEpocSeconds: Long, + ) + + suspend fun onTunnelFailureRecovered(coroutineScope: CoroutineScope) } private val job = ConflatedJob() @@ -103,14 +107,14 @@ class WireguardHandshakeMonitor @Inject constructor( logcat { "Last handshake was already reported, skipping" } } listeners.getPlugins().forEach { - it.onTunnelFailure(lastHandshakeEpocSeconds) + it.onTunnelFailure(this, lastHandshakeEpocSeconds) } } else if (diff.seconds.inWholeMinutes <= REPORT_TUNNEL_FAILURE_RECOVERY_THRESHOLD_MINUTES) { if (failureReported.getAndSet(false)) { logcat(WARN) { "Recovered from tunnel failure" } pixels.reportTunnelFailureRecovered() listeners.getPlugins().forEach { - it.onTunnelFailureRecovered() + it.onTunnelFailureRecovered(this) } } } diff --git a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/failure/FailureRecoveryHandlerTest.kt b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/failure/FailureRecoveryHandlerTest.kt index cd2b501c089b..9a2eeb5ade71 100644 --- a/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/failure/FailureRecoveryHandlerTest.kt +++ b/network-protection/network-protection-impl/src/test/java/com/duckduckgo/networkprotection/impl/failure/FailureRecoveryHandlerTest.kt @@ -16,6 +16,7 @@ package com.duckduckgo.networkprotection.impl.failure +import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.mobile.android.vpn.VpnFeaturesRegistry import com.duckduckgo.networkprotection.impl.CurrentTimeProvider import com.duckduckgo.networkprotection.impl.NetPVpnFeature @@ -27,21 +28,27 @@ import com.wireguard.config.Config import com.wireguard.crypto.KeyPair import java.io.BufferedReader import java.io.StringReader +import java.util.concurrent.TimeUnit import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull -import org.mockito.kotlin.atMost import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever class FailureRecoveryHandlerTest { + + @get:Rule + val coroutineTestRule: CoroutineTestRule = CoroutineTestRule() + @Mock private lateinit var vpnFeaturesRegistry: VpnFeaturesRegistry @@ -91,14 +98,21 @@ class FailureRecoveryHandlerTest { fun setUp() { MockitoAnnotations.openMocks(this) - failureRecoveryHandler = FailureRecoveryHandler(vpnFeaturesRegistry, wgTunnel, wgTunnelConfig, currentTimeProvider, networkProtectionPixels) + failureRecoveryHandler = FailureRecoveryHandler( + vpnFeaturesRegistry, + wgTunnel, + wgTunnelConfig, + currentTimeProvider, + networkProtectionPixels, + coroutineTestRule.testDispatcherProvider, + ) } @Test fun whenDiffFromHandshakeIsBelowThresholdThenDoNothing() = runTest { - whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(300) + whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(TimeUnit.MINUTES.toSeconds(20)) - failureRecoveryHandler.onTunnelFailure(180) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(15)) verifyNoInteractions(vpnFeaturesRegistry) verifyNoInteractions(wgTunnel) @@ -108,22 +122,24 @@ class FailureRecoveryHandlerTest { @Test fun whenOnTunnelFailureRecoveredThenMarkTunnelHealthy() = runTest { - failureRecoveryHandler.onTunnelFailureRecovered() + failureRecoveryHandler.onTunnelFailureRecovered(coroutineTestRule.testScope) verify(wgTunnel).markTunnelHealthy() + verifyNoMoreInteractions(wgTunnel) verifyNoInteractions(networkProtectionPixels) } @Test fun whenFailureRecoveryAndServerChangedThenSetConfigAndRefreshNetp() = runTest { val newConfig = getWgConfig(updatedServerDataDifferentServer) - whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(1080) + whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(TimeUnit.MINUTES.toSeconds(20)) whenever(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)).thenReturn(true) whenever(wgTunnelConfig.getWgConfig()).thenReturn(getWgConfig(defaultServerData)) whenever(wgTunnel.createWgConfig(anyOrNull())).thenReturn(Result.success(newConfig)) - failureRecoveryHandler.onTunnelFailure(180) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + // Only first recovery attempt should happen right away verify(wgTunnel).markTunnelUnhealthy() verify(wgTunnel).markTunnelHealthy() verify(wgTunnelConfig).setWgConfig(newConfig) @@ -136,13 +152,14 @@ class FailureRecoveryHandlerTest { @Test fun whenFailureRecoveryAndTunnelAddressChangedThenSetConfigAndRefreshNetp() = runTest { val newConfig = getWgConfig(updatedServerDataDifferentAddress) - whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(1080) + whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(TimeUnit.MINUTES.toSeconds(20)) whenever(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)).thenReturn(true) whenever(wgTunnelConfig.getWgConfig()).thenReturn(getWgConfig(defaultServerData)) whenever(wgTunnel.createWgConfig(anyOrNull())).thenReturn(Result.success(newConfig)) - failureRecoveryHandler.onTunnelFailure(180) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + // Only first recovery attempt should happen right away verify(wgTunnel).markTunnelUnhealthy() verify(wgTunnel).markTunnelHealthy() verify(wgTunnelConfig).setWgConfig(newConfig) @@ -154,13 +171,14 @@ class FailureRecoveryHandlerTest { @Test fun whenFailureRecoveryAndServerDidnotChangedThenDoNothing() = runTest { - whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(1080) + whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(TimeUnit.MINUTES.toSeconds(20)) whenever(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)).thenReturn(true) whenever(wgTunnelConfig.getWgConfig()).thenReturn(getWgConfig(defaultServerData)) whenever(wgTunnel.createWgConfig(anyOrNull())).thenReturn(Result.success(getWgConfig(defaultServerData))) - failureRecoveryHandler.onTunnelFailure(180) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + // Only first recovery attempt should happen right away verify(wgTunnel).markTunnelUnhealthy() verify(wgTunnel, never()).markTunnelHealthy() verify(wgTunnelConfig, never()).setWgConfig(any()) @@ -169,31 +187,18 @@ class FailureRecoveryHandlerTest { verify(networkProtectionPixels).reportFailureRecoveryCompletedWithServerHealthy() } - @Test - fun whenFailureRecoveryAndCreateConfigFailedThenAttemptMax5TimesOnly() = runTest { - whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(1080) - whenever(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)).thenReturn(true) - whenever(wgTunnelConfig.getWgConfig()).thenReturn(getWgConfig(defaultServerData)) - whenever(wgTunnel.createWgConfig(any())).thenReturn(Result.failure(RuntimeException())) - - failureRecoveryHandler.onTunnelFailure(180) - - verify(wgTunnel, atMost(5)).markTunnelUnhealthy() - verify(networkProtectionPixels, atMost(5)).reportFailureRecoveryStarted() - verify(networkProtectionPixels, atMost(5)).reportFailureRecoveryFailed() - } - @Test fun whenOnTunnelFailureCalledTwiceThenAttemptRecoveryOnceOnly() = runTest { val newConfig = getWgConfig(updatedServerDataDifferentServer) - whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(1080) + whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(TimeUnit.MINUTES.toSeconds(20)) whenever(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)).thenReturn(true) whenever(wgTunnelConfig.getWgConfig()).thenReturn(getWgConfig(defaultServerData)) whenever(wgTunnel.createWgConfig(anyOrNull())).thenReturn(Result.success(newConfig)) - failureRecoveryHandler.onTunnelFailure(180) - failureRecoveryHandler.onTunnelFailure(180) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + // Only first recovery attempt should happen right away. The second call should not do anything verify(wgTunnel).markTunnelUnhealthy() verify(wgTunnel).markTunnelHealthy() verify(wgTunnelConfig).setWgConfig(newConfig) @@ -206,14 +211,15 @@ class FailureRecoveryHandlerTest { @Test fun whenOnTunnelFailureCalledTwiceAndDifferentTunnelAddressThenAttemptRecoveryOnceOnly() = runTest { val newConfig = getWgConfig(updatedServerDataDifferentAddress) - whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(1080) + whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(TimeUnit.MINUTES.toSeconds(20)) whenever(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)).thenReturn(true) whenever(wgTunnelConfig.getWgConfig()).thenReturn(getWgConfig(defaultServerData)) whenever(wgTunnel.createWgConfig(anyOrNull())).thenReturn(Result.success(newConfig)) - failureRecoveryHandler.onTunnelFailure(180) - failureRecoveryHandler.onTunnelFailure(180) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + // Only first recovery attempt should happen right away. The second call should not do anything verify(wgTunnel).markTunnelUnhealthy() verify(wgTunnel).markTunnelHealthy() verify(wgTunnelConfig).setWgConfig(newConfig) @@ -226,15 +232,17 @@ class FailureRecoveryHandlerTest { @Test fun whenOnTunnelFailureCalledAfterRecoveryThenAttemptRecoveryTwice() = runTest { val newConfig = getWgConfig(updatedServerDataDifferentServer) - whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(1080) + whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(TimeUnit.MINUTES.toSeconds(20)) whenever(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)).thenReturn(true) whenever(wgTunnelConfig.getWgConfig()).thenReturn(getWgConfig(defaultServerData)) whenever(wgTunnel.createWgConfig(anyOrNull())).thenReturn(Result.success(newConfig)) - failureRecoveryHandler.onTunnelFailure(180) - failureRecoveryHandler.onTunnelFailureRecovered() - failureRecoveryHandler.onTunnelFailure(180) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + failureRecoveryHandler.onTunnelFailureRecovered(coroutineTestRule.testScope) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + // Only first recovery attempt should happen right away. Since onTunnelFailureRecovered is called, + // calling onTunnelFailure twice here should attempt recovery 2 times. verify(wgTunnel, times(2)).markTunnelUnhealthy() verify(wgTunnel, times(3)).markTunnelHealthy() verify(wgTunnelConfig, times(2)).setWgConfig(newConfig) @@ -246,15 +254,17 @@ class FailureRecoveryHandlerTest { @Test fun whenOnTunnelFailureCalledAfterRecoveryAndDifferentTunnelAddrThenAttemptRecoveryTwice() = runTest { val newConfig = getWgConfig(updatedServerDataDifferentAddress) - whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(1080) + whenever(currentTimeProvider.getTimeInEpochSeconds()).thenReturn(TimeUnit.MINUTES.toSeconds(20)) whenever(vpnFeaturesRegistry.isFeatureRegistered(NetPVpnFeature.NETP_VPN)).thenReturn(true) whenever(wgTunnelConfig.getWgConfig()).thenReturn(getWgConfig(defaultServerData)) whenever(wgTunnel.createWgConfig(anyOrNull())).thenReturn(Result.success(newConfig)) - failureRecoveryHandler.onTunnelFailure(180) - failureRecoveryHandler.onTunnelFailureRecovered() - failureRecoveryHandler.onTunnelFailure(180) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + failureRecoveryHandler.onTunnelFailureRecovered(coroutineTestRule.testScope) + failureRecoveryHandler.onTunnelFailure(coroutineTestRule.testScope, TimeUnit.MINUTES.toSeconds(3)) + // Only first recovery attempt should happen right away. Since onTunnelFailureRecovered is called, + // calling onTunnelFailure twice here should attempt recovery 2 times. verify(wgTunnel, times(2)).markTunnelUnhealthy() verify(wgTunnel, times(3)).markTunnelHealthy() verify(wgTunnelConfig, times(2)).setWgConfig(newConfig) From b972acd641674a1f74cedf808a8934f1002232df Mon Sep 17 00:00:00 2001 From: Karl Dimla Date: Tue, 16 Jul 2024 08:59:35 +0200 Subject: [PATCH 07/20] Add additional metadata for breakage pixel (#4764) Task/Issue URL: https://app.asana.com/0/488551667048375/1207809479817019/f ### Description See attached task description ### Steps to test this PR QA optional --- .../mobile/android/vpn/bugreport/DeviceInfoCollector.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/bugreport/DeviceInfoCollector.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/bugreport/DeviceInfoCollector.kt index 447d11b3c087..dd258e99a088 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/bugreport/DeviceInfoCollector.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/bugreport/DeviceInfoCollector.kt @@ -44,6 +44,7 @@ class DeviceInfoCollector @Inject constructor( put("buildFlavor", appBuildConfig.flavor.toString()) put("os", appBuildConfig.sdkInt) put("batteryOptimizations", (!isIgnoringBatteryOptimizations.get()).toString()) + put("man", appBuildConfig.manufacturer) } } } From 853e1d86bc5d8b5666dac6838d414d2b5af8d4b5 Mon Sep 17 00:00:00 2001 From: Josh Leibstein Date: Tue, 16 Jul 2024 13:27:58 +0100 Subject: [PATCH 08/20] Ignore FingerprintProtectionTest property order (#4767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/488551667048375/1207806134837281/f ### Description Ignore the property order in `FingerprintProtectionTest` ### Steps to test this PR - [x] Privacy tests pass: https://github.com/duckduckgo/Android/actions/runs/9955739865 ✅ --- .../espresso/privacy/FingerprintProtectionTest.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/espresso/privacy/FingerprintProtectionTest.kt b/app/src/androidTest/java/com/duckduckgo/espresso/privacy/FingerprintProtectionTest.kt index 2277b7161b39..f2cb5021fda8 100644 --- a/app/src/androidTest/java/com/duckduckgo/espresso/privacy/FingerprintProtectionTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/espresso/privacy/FingerprintProtectionTest.kt @@ -77,7 +77,9 @@ class FingerprintProtectionTest { val testJson: TestJson? = getTestJson(results.toJSONString()) testJson?.value?.map { if (compatibleIds.contains(it.id)) { - assertEquals(compatibleIds[it.id], it.value.toString()) + val expected = compatibleIds[it.id]!! + val actual = it.value.toString() + assertEquals(sortProperties(expected), sortProperties(actual)) } } IdlingRegistry.getInstance().unregister(idlingResourceForDisableProtections, idlingResourceForScript) @@ -89,6 +91,17 @@ class FingerprintProtectionTest { return jsonAdapter.fromJson(jsonString) } + private fun sortProperties(value: String): String { + return if (value.startsWith("{") && value.endsWith("}")) { + value.trim('{', '}') + .split(", ") + .sorted() + .joinToString(prefix = "{", postfix = "}", separator = ", ") + } else { + value + } + } + companion object { const val SCRIPT = "return results.results;" val compatibleIds = mapOf( From 9ee5049311a515b143903887f9fe598487343c93 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Tue, 16 Jul 2024 09:15:32 -0400 Subject: [PATCH 09/20] Update content scope scripts to version 6.2.0 (#4766) Task/Issue URL: https://app.asana.com/0/488551667048375/1207819010442214/f ----- - Automated content scope scripts dependency update This PR updates the content scope scripts dependency to the latest available version and copies the necessary files. If tests have failed, see https://app.asana.com/0/1202561462274611/1203986899650836/f for further information on what to do next. - [x] All tests must pass Co-authored-by: daxmobile --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b04f4d4adceb..b9009e09c263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@duckduckgo/autoconsent": "^10.10.0", "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#12.0.1", - "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.0.0", + "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.2.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1720621685" }, @@ -68,7 +68,7 @@ "hasInstallScript": true }, "node_modules/@duckduckgo/content-scope-scripts": { - "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#9c65477457126ab7ad963a32b7f85ce08e6bd1a7", + "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#a09f3935ad4e1208405eaed22cb67c2a9215124d", "hasInstallScript": true, "workspaces": [ "packages/special-pages", diff --git a/package.json b/package.json index a7e2dd7d88e4..93d0354db2b3 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dependencies": { "@duckduckgo/autoconsent": "^10.10.0", "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#12.0.1", - "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.0.0", + "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.2.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1720621685" } From fd27e3f1b0eb21b95cf4d15ed7c88b6c3238da44 Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Wed, 17 Jul 2024 10:09:20 +0100 Subject: [PATCH 10/20] Add fastlane lane for updating GitHub release notes (using `gh`) (#4771) Task/Issue URL: https://app.asana.com/0/608920331025315/1207811052233538/f ### Description Adds fastlane lane for updating GitHub release notes ### Steps to test this PR QA-optional --- fastlane/Fastfile | 21 +++++++++++++++++++++ fastlane/README.md | 8 ++++++++ 2 files changed, 29 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 80fdf1b08a36..d5447f36588e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -117,6 +117,27 @@ platform :android do end + desc "Update GitHub release notes" + lane :update_release_notes_github do |options| + + options_release_number = options[:release_number] + options_release_notes = options[:release_notes] + options_notes_type = options[:notes_type] + + newVersion = determine_version_number( + release_number: options_release_number + ) + releaseNotes = determine_release_notes( + release_notes: options_release_notes, + notes_type: options_notes_type + ) + + formatted = "## What's new:\n\n#{releaseNotes}" + UI.message("\n#{formatted} for release #{newVersion}") + + sh "gh release edit #{newVersion} -n \"#{formatted}\"" + end + desc "Annotate release" private_lane :annotate_release do diff --git a/fastlane/README.md b/fastlane/README.md index c48a212559ed..e1655856e185 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -31,6 +31,14 @@ Upload APK to Play Store, in production track with a very small rollout percenta Update Play Store release notes +### android update_release_notes_github + +```sh +[bundle exec] fastlane android update_release_notes_github +``` + +Update GitHub release notes + ### android deploy_dogfood ```sh From 6d8de9a0dd6d42b222e28ca84eded600af4e1754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Wed, 17 Jul 2024 13:57:59 +0200 Subject: [PATCH 11/20] Delay adding FocusedView until it's necessary (#4772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1157893581871903/1207816154536989/f ### Description Delay the addition of the FocusedView until it’s necessary ### Steps to test this PR _Enable FocusedView_ - [x] Fresh install, add some favourites - [x] Visit a site so it’s setup for the next time the app opens - [x] Force close and open it up again - [x] Tap on the omnibar - [x] Verify that FocusedView is visible - [x] Open New Tab - [x] Visit a site - [x] Tap on the omnibar - [x] Verify FocusedView is visible --- .../app/browser/BrowserTabFragment.kt | 42 ++++++++++++------- .../main/res/layout/fragment_browser_tab.xml | 1 + 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index c4a7925e4897..268663064d19 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -830,7 +830,6 @@ class BrowserTabFragment : configureOmnibarTextInput() configureFindInPage() configureAutoComplete() - configureFocusedView() configureNewTab() initPrivacyProtectionsPopup() @@ -2128,18 +2127,6 @@ class BrowserTabFragment : binding.autoCompleteSuggestionsList.adapter = autoCompleteSuggestionsAdapter } - private fun configureFocusedView() { - focusedViewProvider.provideFocusedViewVersion().onEach { focusedView -> - binding.focusedViewContainerLayout.addView( - focusedView.getView(requireContext()), - LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT, - ), - ) - }.launchIn(lifecycleScope) - } - private fun configureNewTab() { newBrowserTab.newTabLayout.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> if (omnibar.omniBarContainer.isPressed) { @@ -3563,20 +3550,43 @@ class BrowserTabFragment : // viewState.showFavourites needs to be moved to FocusedViewModel if (viewState.showSuggestions || viewState.showFavorites) { if (viewState.favorites.isNotEmpty() && viewState.showFavorites) { + showFocusedView() binding.autoCompleteSuggestionsList.gone() - binding.focusedViewContainerLayout.show() } else { binding.autoCompleteSuggestionsList.show() - binding.focusedViewContainerLayout.gone() autoCompleteSuggestionsAdapter.updateData(viewState.searchResults.query, viewState.searchResults.suggestions) + hideFocusedView() } } else { binding.autoCompleteSuggestionsList.gone() - binding.focusedViewContainerLayout.gone() + hideFocusedView() } } } + private fun showFocusedView() { + binding.focusedViewContainerLayout.show() + configureFocusedView() + } + + private fun configureFocusedView() { + if (binding.focusedViewContainerLayout.childCount == 0) { + focusedViewProvider.provideFocusedViewVersion().onEach { focusedView -> + binding.focusedViewContainerLayout.addView( + focusedView.getView(requireContext()), + LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT, + ), + ) + }.launchIn(lifecycleScope) + } + } + + private fun hideFocusedView() { + binding.focusedViewContainerLayout.gone() + } + fun renderOmnibar(viewState: OmnibarViewState) { renderIfChanged(viewState, lastSeenOmnibarViewState) { lastSeenOmnibarViewState = viewState diff --git a/app/src/main/res/layout/fragment_browser_tab.xml b/app/src/main/res/layout/fragment_browser_tab.xml index d76d4125af8a..a7f152f57415 100644 --- a/app/src/main/res/layout/fragment_browser_tab.xml +++ b/app/src/main/res/layout/fragment_browser_tab.xml @@ -51,6 +51,7 @@ android:id="@+id/focusedViewContainerLayout" android:clipToPadding="false" android:elevation="4dp" + android:background="?attr/daxColorSurface" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> From 1a9b5ba8431923a9acb5c0c3ae56f487c5a45172 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:30:39 -0400 Subject: [PATCH 12/20] Update content scope scripts to version 6.3.0 (#4770) Task/Issue URL: https://app.asana.com/0/488551667048375/1207828754892904/f ----- - Automated content scope scripts dependency update This PR updates the content scope scripts dependency to the latest available version and copies the necessary files. If tests have failed, see https://app.asana.com/0/1202561462274611/1203986899650836/f for further information on what to do next. - [x] All tests must pass Co-authored-by: daxmobile --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9009e09c263..6de6e41cfaef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@duckduckgo/autoconsent": "^10.10.0", "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#12.0.1", - "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.2.0", + "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.3.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1720621685" }, @@ -68,7 +68,7 @@ "hasInstallScript": true }, "node_modules/@duckduckgo/content-scope-scripts": { - "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#a09f3935ad4e1208405eaed22cb67c2a9215124d", + "resolved": "git+ssh://git@github.com/duckduckgo/content-scope-scripts.git#dc26bfc6e33ad9c79a719b7f21d5ca0564db1859", "hasInstallScript": true, "workspaces": [ "packages/special-pages", @@ -205,9 +205,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -630,9 +630,9 @@ } }, "node_modules/terser": { - "version": "5.31.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.2.tgz", - "integrity": "sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw==", + "version": "5.31.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.3.tgz", + "integrity": "sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", diff --git a/package.json b/package.json index 93d0354db2b3..b5135c5e17f6 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dependencies": { "@duckduckgo/autoconsent": "^10.10.0", "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#12.0.1", - "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.2.0", + "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.3.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1720621685" } From fee754ac1eaeb422e03afad68119ba1c1a417a88 Mon Sep 17 00:00:00 2001 From: Dax Mobile <44842493+daxmobile@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:31:45 -0400 Subject: [PATCH 13/20] Update reference tests to version 1721137609 (#4769) Task/Issue URL: https://app.asana.com/0/488551667048375/1207828713976554/f ----- - Automated reference tests dependency update This PR updates the reference tests dependency to the latest available version and copies the necessary files. If tests have failed, see https://app.asana.com/0/0/1203766026095653/f for further information on what to do next. - [x] All tests must pass Co-authored-by: daxmobile --- app/src/test/resources/reference_tests/brokensites/tests.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/test/resources/reference_tests/brokensites/tests.json b/app/src/test/resources/reference_tests/brokensites/tests.json index b7c03c0fc84c..5cf88c706162 100644 --- a/app/src/test/resources/reference_tests/brokensites/tests.json +++ b/app/src/test/resources/reference_tests/brokensites/tests.json @@ -486,7 +486,7 @@ {"name": "openerContext", "value": "serp"}, {"name": "userRefreshCount", "value": "3"}, {"name": "jsPerformance", "value": "123.45"}, - {"name": "locale", "value": "en"} + {"name": "locale", "value": "en-US"} ], "exceptPlatforms": [ "ios-browser", diff --git a/package-lock.json b/package-lock.json index 6de6e41cfaef..93df22f5bd71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#12.0.1", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.3.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", - "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1720621685" + "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1721137609" }, "devDependencies": { "@rollup/plugin-json": "^4.1.0", @@ -89,7 +89,7 @@ } }, "node_modules/@duckduckgo/privacy-reference-tests": { - "resolved": "git+ssh://git@github.com/duckduckgo/privacy-reference-tests.git#a242bf03ff33b573eb716405b15924cc712d41c1" + "resolved": "git+ssh://git@github.com/duckduckgo/privacy-reference-tests.git#afb4f6128a3b50d53ddcb1897ea1fb4df6858aa1" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", diff --git a/package.json b/package.json index b5135c5e17f6..0860f03b0a0a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,6 @@ "@duckduckgo/autofill": "github:duckduckgo/duckduckgo-autofill#12.0.1", "@duckduckgo/content-scope-scripts": "github:duckduckgo/content-scope-scripts#6.3.0", "@duckduckgo/privacy-dashboard": "github:duckduckgo/privacy-dashboard#3.5.0", - "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1720621685" + "@duckduckgo/privacy-reference-tests": "github:duckduckgo/privacy-reference-tests#1721137609" } } From 416ba23ab7464dad7246706dc5a23adbfeaf3314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Wed, 17 Jul 2024 21:19:46 +0200 Subject: [PATCH 14/20] New Tab: Customise and Settings (#4731) Task/Issue URL: https://app.asana.com/0/72649045549333/1207318836530012/f ### Description Adds new Settings and Customisation Page ### Steps to test this PR Use http://www.jsonblob.com/api/1260881980894863360 for Privacy Config Use http://www.jsonblob.com/api/1235947624862703616 for Remote Messaging Service _Initial state_ - [x] Fresh install - [x] Open New Tab - [x] Verify Remote Message, Favourites and Shortcuts are visible - [x] Verify Bookmarks, Passwords, Downloads and Settings shortcuts are visible - [x] Open New Tab Settings - [x] Verify only Favourites and Shortcuts are visible _Reordering sections_ - [x] Fresh install - [x] Open New Tab Settings - [x] Reorder Favourites and Shortcuts (long press + drag + drop) - [x] Navigate back - [x] Verify Shortcuts and Favourites have changed order _Enabling / Disabling / Reordering Shortcuts_ - [x] Fresh install - [x] Open New Tab Settings - [x] Add AI Chat Shortcut - [x] Navigate back - [x] Verify AI Chat is now visible - [x] Reorder shortcuts (long press + drag + drop) - [x] Open New Tab Settings - [x] Verify Shortcuts have been reordered - [x] Disable some shorcuts - [x] Navigate back - [x] Verify Shortcuts have been reordered _Disabling Shortcuts section_ - [x] Fresh install - [x] Open New Tab Settings - [x] Disable Shortcuts Section - [x] Verify Shortcuts Management section is now disabled - [x] Navigate back - [x] Verify Shortcuts are no longer visible _Disabling Favourites section_ - [x] Fresh install - [x] Open New Tab Settings - [x] Disable Favourites Section - [x] Navigate back - [x] Verify Favourites are no longer visible _Disabling all sections_ - [x] Fresh install - [x] Open New Tab Settings - [x] Disable all Sections - [x] Navigate back - [x] Verify Dax is visible _App Tracking Protectio section_ - [x] Fresh install - [x] Enable App Tracking Protection - [x] Open New Tab - [x] Verify. AppTP section is visible - [x] Open New Tab Settings - [x] Verify AppTP section is visible - [x] Disable AppTP Section - [x] Navigate back - [x] Verify AppTP is no longer visible --- .../vpn/pixels/DeviceShieldPixelNames.kt | 3 + .../android/vpn/pixels/DeviceShieldPixels.kt | 11 + .../AppTrackingProtectionNewTabSettingView.kt | 119 +++ ...ackingProtectionNewTabSettingsViewModel.kt | 64 ++ .../AppTrackingProtectionStateView.kt | 29 +- .../src/main/res/drawable/ic_announce_24.xml | 27 + .../res/layout/view_apptp_settings_item.xml | 27 + .../src/main/res/values/donottranslate.xml | 4 + .../vpn/pixels/DeviceShieldPixelNamesTest.kt | 8 +- ...ngProtectionNewTabSettingsViewModelTest.kt | 141 ++++ .../app/browser/BrowserTabViewModelTest.kt | 40 +- .../duckduckgo/app/browser/BrowserActivity.kt | 12 +- .../app/browser/BrowserTabFragment.kt | 33 +- .../app/browser/BrowserTabViewModel.kt | 22 +- .../browser/newtab/FocusedLegacyViewModel.kt | 2 + .../app/browser/newtab/FocusedView.kt | 5 + .../app/browser/newtab/FocusedViewProvider.kt | 3 +- .../browser/newtab/NewTabLegacyPageView.kt | 3 - .../newtab/NewTabLegacyPageViewModel.kt | 14 +- .../app/browser/newtab/NewTabPageProvider.kt | 3 +- .../java/com/duckduckgo/app/cta/ui/Cta.kt | 8 + .../app/downloads/DownloadsActivity.kt | 3 + .../DownloadsNewTabShortcutPlugin.kt | 78 ++ .../app/downloads/DownloadsScreens.kt | 29 + .../settings/SettingsNewTabShortcutPlugin.kt | 78 ++ .../app/systemsearch/SystemSearchViewModel.kt | 2 + .../res/drawable/ic_shortcut_downloads.xml | 17 + .../res/drawable/ic_shortcut_settings.xml | 18 + .../res/layout/include_new_browser_tab.xml | 8 +- .../include_onboarding_view_dax_dialog.xml | 7 +- app/src/main/res/layout/view_focused_view.xml | 20 +- .../main/res/layout/view_new_tab_legacy.xml | 4 +- app/src/main/res/values/donottranslate.xml | 3 + .../browser/newtab/NewTabPageProviderTest.kt | 125 ++++ .../autofill/api/AutofillScreens.kt | 1 + autofill/autofill-impl/build.gradle | 2 + .../impl/newtab/AutofillNewTabShortcut.kt | 79 ++ .../res/drawable/ic_shortcut_passwords.xml | 17 + .../src/main/res/values/donottranslate.xml | 3 +- .../browser/api/ui/BrowserScreens.kt | 5 + build.gradle | 3 +- code-formatting.gradle | 2 +- .../duckduckgo/common/ui/menu/PopupMenu.kt | 4 +- .../common/ui/view/ViewExtension.kt | 12 + .../background_circular_icon_container.xml | 19 + .../res/drawable/background_rounded_icon.xml | 19 + .../drawable/background_rounded_surface.xml | 2 +- .../res/drawable/selectable_rounded_icon.xml | 25 + .../selectable_rounded_surface_ripple.xml | 25 + .../main/res/values/design-system-colors.xml | 2 + .../res/values/design-system-dimensions.xml | 3 +- .../main/res/values/design-system-theming.xml | 5 +- .../common-ui/src/main/res/values/widgets.xml | 19 +- .../network-protection-impl/build.gradle | 1 + .../newtabpage/api/NewTabPageSectionPlugin.kt | 6 + .../api/NewTabPageSectionSettingsPlugin.kt | 51 ++ .../api/NewTabPageShortcutPlugin.kt | 25 +- .../drawable/ic_bookmarks_open_color_16.xml | 35 - .../res/drawable/ic_placeholder_color_16.xml | 16 - .../src/main/res/values/donottranslate.xml | 1 - new-tab-page/new-tab-page-impl/build.gradle | 11 +- .../src/main/AndroidManifest.xml | 28 + .../impl/RealNewTabPageSectionProvider.kt | 55 +- .../impl/pixels/NewTabPixelNames.kt | 49 ++ .../newtabpage/impl/pixels/NewTabPixels.kt | 132 ++++ .../impl/settings/DragLinearLayout.kt | 707 ++++++++++++++++++ .../impl/settings/ManageShortcutsAdapter.kt | 91 +++ .../impl/settings/NewTabSettingsActivity.kt | 124 +++ .../impl/settings/NewTabSettingsStore.kt | 68 ++ .../impl/settings/NewTabSettingsViewModel.kt | 141 ++++ .../RealNewTabPageSectionSettingsProvider.kt | 58 ++ .../impl/shortcuts/NewTabShortcutDataStore.kt | 67 ++ .../impl/shortcuts/NewTabShortcuts.kt | 81 ++ .../impl/shortcuts/NewTabShortcutsProvider.kt | 74 +- .../QuickAccessDragTouchItemListener.kt | 91 +++ .../impl/shortcuts/ShortcutSectionItemView.kt | 96 +++ .../impl/shortcuts/ShortcutsAdapter.kt | 183 +++-- .../shortcuts/ShortcutsNewTabSectionView.kt | 80 +- .../shortcuts/ShortcutsNewTabSettingView.kt | 97 +++ .../ShortcutsNewTabSettingsViewModel.kt | 68 ++ .../impl/shortcuts/ShortcutsViewModel.kt | 43 +- .../newtabpage/impl/view/NewTabPageView.kt | 154 +++- .../impl/view/NewTabPageViewModel.kt | 75 ++ .../drawable-hdpi/ab_solid_shadow_holo.9.png | Bin 0 -> 192 bytes .../drawable/ab_solid_shadow_holo_flipped.xml | 9 + .../drawable/background_shortcut_selected.xml | 19 + .../background_shortcut_unselected.xml | 19 + .../src/main/res/drawable/ic_options_16.xml | 26 + .../src/main/res/drawable/ic_shortcut_16.xml | 11 + .../res/drawable/ic_shortcut_selected.xml | 10 + .../res/drawable/ic_shortcut_unselected.xml | 10 + .../res/drawable/ic_shortcuts_ai_chat.xml | 18 + .../res/layout/activity_new_tab_settings.xml | 71 ++ ...tcut.xml => row_shortcut_section_item.xml} | 6 +- .../src/main/res/layout/view_new_tab_page.xml | 136 +++- .../main/res/layout/view_new_tab_page_old.xml | 137 ++++ .../res/layout/view_new_tab_page_shimmer.xml | 86 +++ .../view_new_tab_page_shimmer_grid_item.xml | 38 + ...w_new_tab_setting_manage_shortcut_item.xml | 46 ++ .../layout/view_new_tab_shortcuts_section.xml | 12 +- .../view_new_tab_shortcuts_setting_item.xml | 28 + .../res/layout/view_shortcut_section_item.xml | 56 ++ .../res/values/attrs-draglinearlayout.xml | 22 + .../src/main/res/values/atts-new-tab.xml | 23 + .../src/main/res/values/dimens.xml | 20 + .../src/main/res/values/donottranslate.xml | 8 + .../src/main/res/values/styles-new-tab.xml | 26 + .../impl/NewTabPageSectionProviderTest.kt | 152 ++++ .../newtabpage/impl/TestPluginPoints.kt | 272 +++++++ .../impl/pixels/RealNewTabPixelsTest.kt | 238 ++++++ .../impl/settings/NewTabPageViewModelTest.kt | 160 ++++ .../settings/NewTabSetingsViewModelTest.kt | 167 +++++ ...alNewTabPageSectionSettingsProviderTest.kt | 132 ++++ .../shortcuts/NewTabShortcutsProviderTest.kt | 57 ++ .../ShortcutsNewTabSettingsViewModelTest.kt | 68 ++ .../impl/shortcuts/ShortcutsViewModelTest.kt | 61 +- .../messaging/api/RemoteMessageModel.kt | 2 + .../api/RemoteMessagingRepository.kt | 1 + .../impl/AppRemoteMessagingRepository.kt | 14 + .../messaging/impl/RealRemoteMessageModel.kt | 2 + .../impl/newtab/RemoteMessageView.kt | 21 +- .../newtab}/RemoteMessageViewModelTest.kt | 4 +- .../messaging/store/RemoteMessagesDao.kt | 3 + .../savedsites/impl/SavedSitesPixelName.kt | 17 +- .../impl/bookmarks/BookmarksViewModel.kt | 3 + .../impl/newtab/BookmarksShortcut.kt | 50 +- .../newtab/FavouriteNewTabSectionItemView.kt | 150 ++++ .../newtab/FavouritesNewTabSectionView.kt | 208 ++++-- .../FavouritesNewTabSectionViewModel.kt | 66 +- .../newtab/FavouritesNewTabSectionsAdapter.kt | 24 +- .../newtab/FavouritesNewTabSettingView.kt | 112 +++ .../FavouritesNewTabSettingsViewModel.kt | 69 ++ ...und_circular_32dp_shape_icon_container.xml | 24 + .../favourite_new_tab_favicon_background.xml | 25 + ...vourite_new_tab_placeholder_background.xml | 19 + .../src/main/res/drawable/ic_favorite_24.xml | 27 + .../res/drawable/ic_shortcut_bookmarks.xml | 25 + ...e_circular_32dp_shape_container_ripple.xml | 25 + .../res/layout/row_favourite_section_item.xml | 21 + .../layout/view_favourite_section_item.xml | 70 ++ .../layout/view_favourites_settings_item.xml | 27 + .../view_new_tab_favourites_section.xml | 41 +- .../view_new_tab_favourites_setting_item.xml | 27 + .../view_new_tab_favourites_tooltip.xml | 13 +- .../src/main/res/values/attrs-saves-sites.xml | 37 + .../src/main/res/values/dimen-saved-sites.xml | 19 + .../src/main/res/values/donottranslate.xml | 5 +- .../main/res/values/styles-saved-sites.xml | 25 + .../FavouritesNewTabSectionViewModelTests.kt | 40 +- .../FavouritesNewTabSettingsViewModelTest.kt | 125 ++++ 150 files changed, 6917 insertions(+), 493 deletions(-) create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingView.kt create mode 100644 app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingsViewModel.kt rename app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/{report => newtab}/AppTrackingProtectionStateView.kt (88%) create mode 100644 app-tracking-protection/vpn-impl/src/main/res/drawable/ic_announce_24.xml create mode 100644 app-tracking-protection/vpn-impl/src/main/res/layout/view_apptp_settings_item.xml create mode 100644 app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingsViewModelTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/downloads/DownloadsNewTabShortcutPlugin.kt create mode 100644 app/src/main/java/com/duckduckgo/app/downloads/DownloadsScreens.kt create mode 100644 app/src/main/java/com/duckduckgo/app/settings/SettingsNewTabShortcutPlugin.kt create mode 100644 app/src/main/res/drawable/ic_shortcut_downloads.xml create mode 100644 app/src/main/res/drawable/ic_shortcut_settings.xml create mode 100644 app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabPageProviderTest.kt create mode 100644 autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/newtab/AutofillNewTabShortcut.kt create mode 100644 autofill/autofill-impl/src/main/res/drawable/ic_shortcut_passwords.xml create mode 100644 common/common-ui/src/main/res/drawable/background_circular_icon_container.xml create mode 100644 common/common-ui/src/main/res/drawable/background_rounded_icon.xml create mode 100644 common/common-ui/src/main/res/drawable/selectable_rounded_icon.xml create mode 100644 common/common-ui/src/main/res/drawable/selectable_rounded_surface_ripple.xml create mode 100644 new-tab-page/new-tab-page-api/src/main/java/com/duckduckgo/newtabpage/api/NewTabPageSectionSettingsPlugin.kt delete mode 100644 new-tab-page/new-tab-page-api/src/main/res/drawable/ic_bookmarks_open_color_16.xml delete mode 100644 new-tab-page/new-tab-page-api/src/main/res/drawable/ic_placeholder_color_16.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/AndroidManifest.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/pixels/NewTabPixelNames.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/pixels/NewTabPixels.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/settings/DragLinearLayout.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/settings/ManageShortcutsAdapter.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/settings/NewTabSettingsActivity.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/settings/NewTabSettingsStore.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/settings/NewTabSettingsViewModel.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/settings/RealNewTabPageSectionSettingsProvider.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/shortcuts/NewTabShortcutDataStore.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/shortcuts/NewTabShortcuts.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/shortcuts/QuickAccessDragTouchItemListener.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutSectionItemView.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsNewTabSettingView.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsNewTabSettingsViewModel.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/view/NewTabPageViewModel.kt create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/drawable-hdpi/ab_solid_shadow_holo.9.png create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/drawable/ab_solid_shadow_holo_flipped.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/drawable/background_shortcut_selected.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/drawable/background_shortcut_unselected.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/drawable/ic_options_16.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/drawable/ic_shortcut_16.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/drawable/ic_shortcut_selected.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/drawable/ic_shortcut_unselected.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/drawable/ic_shortcuts_ai_chat.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/layout/activity_new_tab_settings.xml rename new-tab-page/new-tab-page-impl/src/main/res/layout/{view_chat_shortcut.xml => row_shortcut_section_item.xml} (76%) create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/layout/view_new_tab_page_old.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/layout/view_new_tab_page_shimmer.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/layout/view_new_tab_page_shimmer_grid_item.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/layout/view_new_tab_setting_manage_shortcut_item.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/layout/view_new_tab_shortcuts_setting_item.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/layout/view_shortcut_section_item.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/values/attrs-draglinearlayout.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/values/atts-new-tab.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/values/dimens.xml create mode 100644 new-tab-page/new-tab-page-impl/src/main/res/values/styles-new-tab.xml create mode 100644 new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/NewTabPageSectionProviderTest.kt create mode 100644 new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/TestPluginPoints.kt create mode 100644 new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/pixels/RealNewTabPixelsTest.kt create mode 100644 new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/NewTabPageViewModelTest.kt create mode 100644 new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/NewTabSetingsViewModelTest.kt create mode 100644 new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/RealNewTabPageSectionSettingsProviderTest.kt create mode 100644 new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/NewTabShortcutsProviderTest.kt create mode 100644 new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsNewTabSettingsViewModelTest.kt rename {app/src/test/java/com/duckduckgo/app/browser/remotemessage => remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/newtab}/RemoteMessageViewModelTest.kt (99%) create mode 100644 saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouriteNewTabSectionItemView.kt create mode 100644 saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingView.kt create mode 100644 saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingsViewModel.kt create mode 100644 saved-sites/saved-sites-impl/src/main/res/drawable/background_circular_32dp_shape_icon_container.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/drawable/favourite_new_tab_favicon_background.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/drawable/favourite_new_tab_placeholder_background.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/drawable/ic_favorite_24.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/drawable/ic_shortcut_bookmarks.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/drawable/selectable_circular_32dp_shape_container_ripple.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/layout/row_favourite_section_item.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/layout/view_favourite_section_item.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/layout/view_favourites_settings_item.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_setting_item.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/values/attrs-saves-sites.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/values/dimen-saved-sites.xml create mode 100644 saved-sites/saved-sites-impl/src/main/res/values/styles-saved-sites.xml create mode 100644 saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingsViewModelTest.kt diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt index b0bf73130ce5..223488adea8e 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNames.kt @@ -226,5 +226,8 @@ enum class DeviceShieldPixelNames(override val pixelName: String, val enqueue: B VPN_START_ATTEMPT("m_vpn_ev_start_attempt_c", enqueue = true), VPN_START_ATTEMPT_SUCCESS("m_vpn_ev_start_attempt_success_c", enqueue = true), VPN_START_ATTEMPT_FAILURE("m_vpn_ev_start_attempt_failure_c", enqueue = true), + + NEW_TAB_SECTION_TOGGLED_OFF("m_new_tab_page_customize_section_off_appTP"), + NEW_TAB_SECTION_TOGGLED_ON("m_new_tab_page_customize_section_on_appTP"), ; } diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt index 83e649258072..0aaf4bce22ec 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixels.kt @@ -363,6 +363,9 @@ interface DeviceShieldPixels { fun reportVpnStartAttempt() fun reportVpnStartAttemptSuccess() + + // New Tab Engagement pixels https://app.asana.com/0/72649045549333/1207667088727866/f + fun reportNewTabSectionToggled(enabled: Boolean) } @ContributesBinding(AppScope::class) @@ -827,6 +830,14 @@ class RealDeviceShieldPixels @Inject constructor( tryToFireDailyPixel(String.format(Locale.US, DeviceShieldPixelNames.REPORT_TLS_PARSING_ERROR_CODE_DAILY.pixelName, errorCode)) } + override fun reportNewTabSectionToggled(enabled: Boolean) { + if (enabled) { + firePixel(DeviceShieldPixelNames.NEW_TAB_SECTION_TOGGLED_ON) + } else { + firePixel(DeviceShieldPixelNames.NEW_TAB_SECTION_TOGGLED_OFF) + } + } + private fun firePixel( p: DeviceShieldPixelNames, payload: Map = emptyMap(), diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingView.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingView.kt new file mode 100644 index 000000000000..e65334ac144d --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingView.kt @@ -0,0 +1,119 @@ +/* + * 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.mobile.android.vpn.ui.newtab + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeViewModelStoreOwner +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.anvil.annotations.PriorityKey +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.common.utils.ViewViewModelFactory +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.di.scopes.ViewScope +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.mobile.android.vpn.databinding.ViewApptpSettingsItemBinding +import com.duckduckgo.mobile.android.vpn.feature.removal.VpnFeatureRemover +import com.duckduckgo.mobile.android.vpn.ui.newtab.AppTrackingProtectionNewTabSettingsViewModel.ViewState +import com.duckduckgo.mobile.android.vpn.ui.onboarding.VpnStore +import com.duckduckgo.newtabpage.api.NewTabPageSection +import com.duckduckgo.newtabpage.api.NewTabPageSectionSettingsPlugin +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.android.support.AndroidSupportInjection +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@InjectWith(ViewScope::class) +class AppTrackingProtectionNewTabSettingView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : LinearLayout(context, attrs, defStyle) { + + @Inject + lateinit var viewModelFactory: ViewViewModelFactory + + private val binding: ViewApptpSettingsItemBinding by viewBinding() + + private var coroutineScope: CoroutineScope? = null + + private val viewModel: AppTrackingProtectionNewTabSettingsViewModel by lazy { + ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[AppTrackingProtectionNewTabSettingsViewModel::class.java] + } + + override fun onAttachedToWindow() { + AndroidSupportInjection.inject(this) + super.onAttachedToWindow() + findViewTreeLifecycleOwner()?.lifecycle?.addObserver(viewModel) + + @SuppressLint("NoHardcodedCoroutineDispatcher") + coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + viewModel.viewState + .onEach { render(it) } + .launchIn(coroutineScope!!) + } + + private fun render(viewState: ViewState) { + binding.root.quietlySetIsChecked(viewState.enabled) { _, enabled -> + viewModel.onSettingEnabled(enabled) + } + } +} + +@ContributesMultibinding(scope = ActivityScope::class) +@PriorityKey(NewTabPageSectionSettingsPlugin.APP_TRACKING_PROTECTION) +class AppTrackingProtectionNewTabSettingViewPlugin @Inject constructor( + private val vpnStore: VpnStore, + private val vpnFeatureRemover: VpnFeatureRemover, +) : NewTabPageSectionSettingsPlugin { + override val name = NewTabPageSection.APP_TRACKING_PROTECTION.name + + override fun getView(context: Context): View { + return AppTrackingProtectionNewTabSettingView(context) + } + + override suspend fun isActive(): Boolean { + if (vpnFeatureRemover.isFeatureRemoved()) { + return false + } + return vpnStore.didShowOnboarding() + } +} + +/** + * Local feature/settings - they will never be in remote config + */ +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "newTabAppTPSectionSetting", +) +interface NewTabAppTrackingProtectionSectionSetting { + @Toggle.DefaultValue(true) + fun self(): Toggle +} diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingsViewModel.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingsViewModel.kt new file mode 100644 index 000000000000..dead0d541f03 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingsViewModel.kt @@ -0,0 +1,64 @@ +/* + * 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.mobile.android.vpn.ui.newtab + +import android.annotation.SuppressLint +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.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.ViewScope +import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle +@ContributesViewModel(ViewScope::class) +class AppTrackingProtectionNewTabSettingsViewModel @Inject constructor( + private val dispatchers: DispatcherProvider, + private val setting: NewTabAppTrackingProtectionSectionSetting, + private val pixel: DeviceShieldPixels, +) : ViewModel(), DefaultLifecycleObserver { + + private val _viewState = MutableStateFlow(ViewState(true)) + val viewState = _viewState.asStateFlow() + + data class ViewState(val enabled: Boolean) + + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + + viewModelScope.launch(dispatchers.io()) { + val isEnabled = setting.self().isEnabled() + withContext(dispatchers.main()) { + _viewState.update { ViewState(isEnabled) } + } + } + } + + fun onSettingEnabled(enabled: Boolean) { + setting.self().setEnabled(State(enabled)) + pixel.reportNewTabSectionToggled(enabled) + } +} diff --git a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/report/AppTrackingProtectionStateView.kt b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionStateView.kt similarity index 88% rename from app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/report/AppTrackingProtectionStateView.kt rename to app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionStateView.kt index ec411cf5e2da..67697780e356 100644 --- a/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/report/AppTrackingProtectionStateView.kt +++ b/app-tracking-protection/vpn-impl/src/main/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionStateView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.mobile.android.vpn.ui.report +package com.duckduckgo.mobile.android.vpn.ui.newtab import android.annotation.SuppressLint import android.content.Context @@ -33,9 +33,13 @@ import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.mobile.android.vpn.R import com.duckduckgo.mobile.android.vpn.databinding.FragmentDeviceShieldCtaBinding +import com.duckduckgo.mobile.android.vpn.feature.removal.VpnFeatureRemover import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnRunningState.ENABLED import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason.REVOKED +import com.duckduckgo.mobile.android.vpn.ui.onboarding.VpnStore +import com.duckduckgo.mobile.android.vpn.ui.report.PrivacyReportViewModel +import com.duckduckgo.mobile.android.vpn.ui.report.PrivacyReportViewModel.PrivacyReportView.TrackersBlocked import com.duckduckgo.mobile.android.vpn.ui.report.PrivacyReportViewModel.PrivacyReportView.ViewState import com.duckduckgo.mobile.android.vpn.ui.tracker_activity.DeviceShieldTrackerActivity import com.duckduckgo.newtabpage.api.NewTabPageSection @@ -128,7 +132,7 @@ class AppTrackingProtectionStateView @JvmOverloads constructor( binding.deviceShieldCtaImage.setImageResource(R.drawable.ic_apptp_warning) } - private fun renderTrackersBlockedWhenEnabled(trackerBlocked: PrivacyReportViewModel.PrivacyReportView.TrackersBlocked) { + private fun renderTrackersBlockedWhenEnabled(trackerBlocked: TrackersBlocked) { val trackersBlocked = trackerBlocked.trackers val lastTrackingApp = trackerBlocked.latestApp val otherApps = trackerBlocked.otherAppsSize @@ -185,11 +189,28 @@ class AppTrackingProtectionStateView @JvmOverloads constructor( @ContributesActivePlugin( AppScope::class, boundType = NewTabPageSectionPlugin::class, + priority = 2, ) -class AppTrackingProtectionNewTabPageSectionPlugin @Inject constructor() : NewTabPageSectionPlugin { +class AppTrackingProtectionNewTabPageSectionPlugin @Inject constructor( + private val vpnStore: VpnStore, + private val vpnFeatureRemover: VpnFeatureRemover, + private val setting: NewTabAppTrackingProtectionSectionSetting, +) : NewTabPageSectionPlugin { override val name = NewTabPageSection.APP_TRACKING_PROTECTION.name override fun getView(context: Context): View { return AppTrackingProtectionStateView(context) } + + override suspend fun isUserEnabled(): Boolean { + if (vpnFeatureRemover.isFeatureRemoved()) { + return false + } + + return if (vpnStore.didShowOnboarding()) { + setting.self().isEnabled() + } else { + false + } + } } diff --git a/app-tracking-protection/vpn-impl/src/main/res/drawable/ic_announce_24.xml b/app-tracking-protection/vpn-impl/src/main/res/drawable/ic_announce_24.xml new file mode 100644 index 000000000000..509b5a3408c4 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/res/drawable/ic_announce_24.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app-tracking-protection/vpn-impl/src/main/res/layout/view_apptp_settings_item.xml b/app-tracking-protection/vpn-impl/src/main/res/layout/view_apptp_settings_item.xml new file mode 100644 index 000000000000..e9ff51e227b7 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/main/res/layout/view_apptp_settings_item.xml @@ -0,0 +1,27 @@ + + + + \ No newline at end of file diff --git a/app-tracking-protection/vpn-impl/src/main/res/values/donottranslate.xml b/app-tracking-protection/vpn-impl/src/main/res/values/donottranslate.xml index 485cd55dc6e1..903b101ce06d 100644 --- a/app-tracking-protection/vpn-impl/src/main/res/values/donottranslate.xml +++ b/app-tracking-protection/vpn-impl/src/main/res/values/donottranslate.xml @@ -37,4 +37,8 @@ Action required! An Android setting needs to be changed for DuckDuckGo VPN. Action required! Change an Android setting to use DuckDuckGo VPN and App Tracking Protection. + + App Tracking Protection Status + + diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNamesTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNamesTest.kt index b9149999523f..5a3bcc817eac 100644 --- a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNamesTest.kt +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/pixels/DeviceShieldPixelNamesTest.kt @@ -22,8 +22,10 @@ import org.junit.Test class DeviceShieldPixelNamesTest { @Test fun allAppTrackingProtectionPixelsShallBePrefixed() { - DeviceShieldPixelNames.values().map { it.pixelName }.forEach { pixel -> - assertTrue(pixel.startsWith("m_atp") || pixel.startsWith("m_vpn")) - } + DeviceShieldPixelNames.values() + .map { it.pixelName } + .forEach { pixel -> + assertTrue(pixel.startsWith("m_atp") || pixel.startsWith("m_vpn") || pixel.startsWith("m_new_tab_page")) + } } } diff --git a/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingsViewModelTest.kt b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingsViewModelTest.kt new file mode 100644 index 000000000000..d06197ff5c71 --- /dev/null +++ b/app-tracking-protection/vpn-impl/src/test/java/com/duckduckgo/mobile/android/vpn/ui/newtab/AppTrackingProtectionNewTabSettingsViewModelTest.kt @@ -0,0 +1,141 @@ +/* + * 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.mobile.android.vpn.ui.newtab + +import androidx.lifecycle.LifecycleOwner +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.mobile.android.vpn.pixels.DeviceShieldPixels +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class AppTrackingProtectionNewTabSettingsViewModelTest { + + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private lateinit var testee: AppTrackingProtectionNewTabSettingsViewModel + private val setting: NewTabAppTrackingProtectionSectionSetting = mock() + private val lifecycleOwner: LifecycleOwner = mock() + private val pixels: DeviceShieldPixels = mock() + + @Before + fun setup() { + testee = AppTrackingProtectionNewTabSettingsViewModel( + coroutinesTestRule.testDispatcherProvider, + setting, + pixels, + ) + } + + @Test + fun whenViewCreatedAndSettingEnabledThenViewStateUpdated() = runTest { + whenever(setting.self()).thenReturn( + object : Toggle { + override fun isEnabled(): Boolean { + return true + } + + override fun setEnabled(state: State) { + } + + override fun getRawStoredState(): State { + return State() + } + }, + ) + testee.onCreate(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.enabled) + } + } + } + + @Test + fun whenViewCreatedAndSettingDisabledThenViewStateUpdated() = runTest { + whenever(setting.self()).thenReturn( + object : Toggle { + override fun isEnabled(): Boolean { + return false + } + + override fun setEnabled(state: State) { + } + + override fun getRawStoredState(): State { + return State() + } + }, + ) + testee.onCreate(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertFalse(it.enabled) + } + } + } + + @Test + fun whenSettingEnabledThenPixelFired() = runTest { + whenever(setting.self()).thenReturn( + object : Toggle { + override fun isEnabled(): Boolean { + return false + } + + override fun setEnabled(state: State) { + } + + override fun getRawStoredState(): State { + return State() + } + }, + ) + testee.onSettingEnabled(true) + verify(pixels).reportNewTabSectionToggled(true) + } + + @Test + fun whenSettingDisabledThenPixelFired() = runTest { + whenever(setting.self()).thenReturn( + object : Toggle { + override fun isEnabled(): Boolean { + return false + } + + override fun setEnabled(state: State) { + } + + override fun getRawStoredState(): State { + return State() + } + }, + ) + testee.onSettingEnabled(false) + verify(pixels).reportNewTabSectionToggled(false) + } +} diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index cc92adedc6af..c621ba795ea3 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -94,7 +94,6 @@ import com.duckduckgo.app.browser.model.LongPressTarget import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter.QuickAccessFavorite import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter -import com.duckduckgo.app.browser.remotemessage.CommandActionMapper import com.duckduckgo.app.browser.remotemessage.RemoteMessagingModel import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.browser.viewstate.BrowserViewState @@ -148,7 +147,6 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.COUNT import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.UNIQUE import com.duckduckgo.app.surrogates.SurrogateResponse -import com.duckduckgo.app.survey.notification.SurveyNotificationScheduler import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.trackerdetection.EntityLookup @@ -174,6 +172,7 @@ import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.history.api.HistoryEntry.VisitedPage import com.duckduckgo.history.api.NavigationHistory +import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels import com.duckduckgo.privacy.config.api.* import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc import com.duckduckgo.privacy.config.impl.features.gpc.RealGpc.Companion.GPC_HEADER @@ -195,8 +194,6 @@ import com.duckduckgo.site.permissions.api.SitePermissionsManager import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissionQueryResponse import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions import com.duckduckgo.subscriptions.api.Subscriptions -import com.duckduckgo.sync.api.engine.SyncEngine -import com.duckduckgo.sync.api.engine.SyncEngine.SyncTrigger.FEATURE_READ import com.duckduckgo.sync.api.favicons.FaviconsFetchingPrompt import com.duckduckgo.voice.api.VoiceSearchAvailability import com.duckduckgo.voice.api.VoiceSearchAvailabilityPixelLogger @@ -307,6 +304,9 @@ class BrowserTabViewModelTest { @Mock private lateinit var mockPixel: Pixel + @Mock + private lateinit var mockNewTabPixels: NewTabPixels + @Mock private lateinit var mockOnboardingStore: OnboardingStore @@ -385,9 +385,6 @@ class BrowserTabViewModelTest { @Mock private lateinit var mockUserAllowListRepository: UserAllowListRepository - @Mock - private lateinit var mockSurveyNotificationScheduler: SurveyNotificationScheduler - @Mock private lateinit var mockFileChooserCallback: ValueCallback> @@ -447,8 +444,6 @@ class BrowserTabViewModelTest { private val mockSitePermissionsManager: SitePermissionsManager = mock() - private val mockSyncEngine: SyncEngine = mock() - private val cameraHardwareChecker: CameraHardwareChecker = mock() private val androidBrowserConfig: AndroidBrowserConfigFeature = mock() @@ -471,7 +466,6 @@ class BrowserTabViewModelTest { private val mockExtendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles = mock() private val mockUserBrowserProperties: UserBrowserProperties = mock() private val mockAutoCompleteRepository: AutoCompleteRepository = mock() - private val commandActionMapper: CommandActionMapper = mock() @Before fun before() { @@ -602,8 +596,6 @@ class BrowserTabViewModelTest { autofillCapabilityChecker = autofillCapabilityChecker, autofillFireproofDialogSuppressor = autofillFireproofDialogSuppressor, automaticSavedLoginsMonitor = automaticSavedLoginsMonitor, - surveyNotificationScheduler = mockSurveyNotificationScheduler, - syncEngine = mockSyncEngine, device = mockDeviceInfo, sitePermissionsManager = mockSitePermissionsManager, cameraHardwareChecker = cameraHardwareChecker, @@ -617,7 +609,7 @@ class BrowserTabViewModelTest { bypassedSSLCertificatesRepository = mockBypassedSSLCertificatesRepository, userBrowserProperties = mockUserBrowserProperties, history = mockNavigationHistory, - commandActionMapper = commandActionMapper, + newTabPixels = mockNewTabPixels, ) testee.loadData("abc", null, false) @@ -4661,17 +4653,6 @@ class BrowserTabViewModelTest { } } - @Test - fun whenNewTabOpenedAndFavouritesPresentThenSyncTriggered() = runTest { - val favoriteSite = Favorite(id = UUID.randomUUID().toString(), title = "", url = "www.example.com", position = 0, lastModified = "timestamp") - favoriteListFlow.send(listOf(favoriteSite)) - loadUrl("www.example.com", isBrowserShowing = true) - - testee.onNewTabFavouritesShown() - - verify(mockSyncEngine).triggerSync(FEATURE_READ) - } - @Test fun whenOnShowFileChooserWithImageWildcardedTypeThenImageOrCameraChooserCommandSent() { val params = buildFileChooserParams(arrayOf("image/*")) @@ -5432,6 +5413,13 @@ class BrowserTabViewModelTest { verify(mockPixel).fire(AppPixelName.KEYBOARD_GO_SERP_CLICKED) } + @Test + fun whenNewTabShownThenPixelIsFired() { + testee.onNewTabShown() + + verify(mockNewTabPixels).fireNewTabDisplayed() + } + private fun aCredential(): LoginCredentials { return LoginCredentials(domain = null, username = null, password = null) } @@ -5586,10 +5574,6 @@ class BrowserTabViewModelTest { testee.ctaViewState.value = ctaViewState().copy(cta = cta) } - private suspend fun givenRemoteMessage(remoteMessage: RemoteMessage) { - remoteMessageFlow.send(remoteMessage) - } - private fun aTabEntity(id: String): TabEntity { return TabEntity(tabId = id, position = 0) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index 9a0717d8c7cb..7d916ef3e7f5 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -41,7 +41,7 @@ import com.duckduckgo.app.browser.databinding.ActivityBrowserBinding import com.duckduckgo.app.browser.databinding.IncludeOmnibarToolbarMockupBinding import com.duckduckgo.app.browser.shortcut.ShortcutBuilder import com.duckduckgo.app.di.AppCoroutineScope -import com.duckduckgo.app.downloads.DownloadsActivity +import com.duckduckgo.app.downloads.DownloadsScreens.DownloadsScreenNoParams import com.duckduckgo.app.feedback.ui.common.FeedbackActivity import com.duckduckgo.app.fire.DataClearer import com.duckduckgo.app.fire.DataClearerForegroundAppRestartPixel @@ -198,8 +198,6 @@ open class BrowserActivity : DuckDuckGoActivity() { Timber.i("Automatic data clearer not yet finished, so deferring processing of intent") lastIntent = intent } - - viewModel.launchFromThirdParty() } private fun initializeServiceWorker() { @@ -337,11 +335,11 @@ open class BrowserActivity : DuckDuckGoActivity() { lifecycleScope.launch { viewModel.onOpenFavoriteFromWidget(query = sharedText) } return } else if (intent.getBooleanExtra(OPEN_IN_CURRENT_TAB_EXTRA, false)) { - Timber.w("New Tab: open in current tab requested") + Timber.w("open in current tab requested") if (currentTab != null) { currentTab?.submitQuery(sharedText) } else { - Timber.w("New Tab: can't use current tab, opening in new tab instead") + Timber.w("can't use current tab, opening in new tab instead") lifecycleScope.launch { viewModel.onOpenInNewTabRequested(query = sharedText, skipHome = true) } } return @@ -350,7 +348,9 @@ open class BrowserActivity : DuckDuckGoActivity() { val selectedText = intent.getBooleanExtra(SELECTED_TEXT_EXTRA, false) val sourceTabId = if (selectedText) currentTab?.tabId else null val skipHome = !selectedText + viewModel.launchFromThirdParty() lifecycleScope.launch { viewModel.onOpenInNewTabRequested(sourceTabId = sourceTabId, query = sharedText, skipHome = skipHome) } + return } } @@ -489,7 +489,7 @@ open class BrowserActivity : DuckDuckGoActivity() { } fun launchDownloads() { - startActivity(DownloadsActivity.intent(this)) + globalActivityStarter.start(this, DownloadsScreenNoParams) } private fun configureOnBackPressedListener() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 268663064d19..64e2e16bfaa0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1147,7 +1147,6 @@ class BrowserTabFragment : } private fun showHome() { - Timber.d("New Tab: showHome") viewModel.onHomeShown() dismissAppLinkSnackBar() errorSnackbar.dismiss() @@ -1163,7 +1162,6 @@ class BrowserTabFragment : } private fun showBrowser() { - Timber.d("New Tab: showBrowser") newBrowserTab.newTabLayout.gone() newBrowserTab.newTabContainerLayout.gone() binding.browserLayout.show() @@ -1178,7 +1176,6 @@ class BrowserTabFragment : errorType: WebViewErrorResponse, url: String?, ) { - Timber.d("New Tab: showError") webViewContainer.gone() newBrowserTab.newTabLayout.gone() newBrowserTab.newTabContainerLayout.gone() @@ -2178,6 +2175,7 @@ class BrowserTabFragment : viewModel.sendPixelsOnBackKeyPressed() omnibar.omnibarTextInput.hideKeyboard() binding.focusDummy.requestFocus() + omnibar.omniBarContainer.isPressed = false // Allow the event to be handled by the next receiver. return false } @@ -2860,6 +2858,7 @@ class BrowserTabFragment : Timber.v("Keyboard now hiding") omnibar.omnibarTextInput.hideKeyboard() binding.focusDummy.requestFocus() + omnibar.omniBarContainer.isPressed = false } } @@ -2868,13 +2867,7 @@ class BrowserTabFragment : Timber.v("Keyboard now hiding") omnibar.omnibarTextInput.postDelayed(KEYBOARD_DELAY) { omnibar.omnibarTextInput?.hideKeyboard() } binding.focusDummy.requestFocus() - } - } - - private fun showKeyboardImmediately() { - if (!isHidden) { - Timber.v("Keyboard now showing") - omnibar.omnibarTextInput?.showKeyboard() + omnibar.omniBarContainer.isPressed = false } } @@ -2882,6 +2875,7 @@ class BrowserTabFragment : if (!isHidden) { Timber.v("Keyboard now showing") omnibar.omnibarTextInput.postDelayed(KEYBOARD_DELAY) { omnibar.omnibarTextInput?.showKeyboard() } + omnibar.omniBarContainer.isPressed = true } } @@ -3946,23 +3940,24 @@ class BrowserTabFragment : } private fun showNewTab() { - Timber.d("New Tab: showNewTab") newTabPageProvider.provideNewTabPageVersion().onEach { newTabPage -> - newBrowserTab.newTabContainerLayout.addView( - newTabPage.getView(requireContext()), - LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT, - ), - ) + if (newBrowserTab.newTabContainerLayout.childCount == 0) { + newBrowserTab.newTabContainerLayout.addView( + newTabPage.getView(requireContext()), + LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT, + ), + ) + } } .launchIn(lifecycleScope) newBrowserTab.newTabContainerLayout.show() newBrowserTab.newTabLayout.show() + viewModel.onNewTabShown() } private fun hideNewTab() { - Timber.d("New Tab: hideNewTab") newBrowserTab.newTabContainerLayout.gone() } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 17667c255357..bc11c910334a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -90,7 +90,6 @@ import com.duckduckgo.app.browser.newtab.FavoritesQuickAccessAdapter import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.omnibar.QueryOrigin import com.duckduckgo.app.browser.omnibar.QueryOrigin.FromAutocomplete -import com.duckduckgo.app.browser.remotemessage.CommandActionMapper import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.browser.urlextraction.UrlExtractionListener import com.duckduckgo.app.browser.viewstate.AccessibilityViewState @@ -142,9 +141,9 @@ import com.duckduckgo.app.statistics.api.StatisticsUpdater import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.COUNT +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.UNIQUE import com.duckduckgo.app.surrogates.SurrogateResponse -import com.duckduckgo.app.survey.notification.SurveyNotificationScheduler import com.duckduckgo.app.tabs.model.TabEntity import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.trackerdetection.model.TrackingEvent @@ -172,6 +171,7 @@ import com.duckduckgo.downloads.api.FileDownloader import com.duckduckgo.downloads.api.FileDownloader.PendingFileDownload import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.js.messaging.api.JsCallbackData +import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels import com.duckduckgo.privacy.config.api.* import com.duckduckgo.privacy.dashboard.impl.pixels.PrivacyDashboardPixels import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupExperimentExternalPixels @@ -190,8 +190,6 @@ import com.duckduckgo.site.permissions.api.SitePermissionsManager import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissionQueryResponse import com.duckduckgo.site.permissions.api.SitePermissionsManager.SitePermissions import com.duckduckgo.subscriptions.api.Subscriptions -import com.duckduckgo.sync.api.engine.SyncEngine -import com.duckduckgo.sync.api.engine.SyncEngine.SyncTrigger.FEATURE_READ import com.duckduckgo.sync.api.favicons.FaviconsFetchingPrompt import com.duckduckgo.voice.api.VoiceSearchAvailability import com.duckduckgo.voice.api.VoiceSearchAvailabilityPixelLogger @@ -255,10 +253,8 @@ class BrowserTabViewModel @Inject constructor( private val adClickManager: AdClickManager, private val autofillFireproofDialogSuppressor: AutofillFireproofDialogSuppressor, private val automaticSavedLoginsMonitor: AutomaticSavedLoginsMonitor, - private val surveyNotificationScheduler: SurveyNotificationScheduler, private val device: DeviceInfo, private val sitePermissionsManager: SitePermissionsManager, - private val syncEngine: SyncEngine, private val cameraHardwareChecker: CameraHardwareChecker, private val androidBrowserConfig: AndroidBrowserConfigFeature, private val privacyProtectionsPopupManager: PrivacyProtectionsPopupManager, @@ -270,7 +266,7 @@ class BrowserTabViewModel @Inject constructor( private val bypassedSSLCertificatesRepository: BypassedSSLCertificatesRepository, private val userBrowserProperties: UserBrowserProperties, private val history: NavigationHistory, - private val commandActionMapper: CommandActionMapper, + private val newTabPixels: NewTabPixels, ) : WebViewClientListener, EditSavedSiteListener, DeleteBookmarkListener, @@ -1930,6 +1926,7 @@ class BrowserTabViewModel @Inject constructor( onDeleteFavoriteRequested(favorite) } else { pixel.fire(AppPixelName.MENU_ACTION_ADD_FAVORITE_PRESSED.pixelName) + pixel.fire(SavedSitesPixelName.MENU_ACTION_ADD_FAVORITE_PRESSED_DAILY.pixelName, type = DAILY) saveFavoriteSite(url, title ?: "") } } @@ -2080,6 +2077,7 @@ class BrowserTabViewModel @Inject constructor( override fun onFavoriteAdded() { pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) + pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY) } override fun onFavoriteRemoved() { @@ -2432,12 +2430,6 @@ class BrowserTabViewModel @Inject constructor( } } - fun onNewTabFavouritesShown() { - viewModelScope.launch(dispatchers.io()) { - syncEngine.triggerSync(FEATURE_READ) - } - } - suspend fun refreshCta(): Cta? { if (currentGlobalLayoutState() is Browser) { val isBrowserShowing = currentBrowserViewState().browserShowing @@ -3336,6 +3328,10 @@ class BrowserTabViewModel @Inject constructor( return URLUtil.isNetworkUrl(text) || URLUtil.isAssetUrl(text) || URLUtil.isFileUrl(text) || URLUtil.isContentUrl(text) } + fun onNewTabShown() { + newTabPixels.fireNewTabDisplayed() + } + companion object { private const val FIXED_PROGRESS = 50 diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyViewModel.kt index 66014da84603..0c8fe15be07b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedLegacyViewModel.kt @@ -28,6 +28,7 @@ import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.Command.DeleteSa import com.duckduckgo.app.browser.newtab.FocusedLegacyViewModel.Command.ShowEditSavedSiteDialog import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.savedsites.api.SavedSitesRepository @@ -210,6 +211,7 @@ class FocusedLegacyViewModel @Inject constructor( override fun onFavoriteAdded() { pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) + pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY, type = DAILY) } override fun onFavoriteRemoved() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedView.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedView.kt index d4330154ab85..0ff2ef6c5d71 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedView.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedView.kt @@ -32,4 +32,9 @@ class FocusedView @JvmOverloads constructor( ) : LinearLayout(context, attrs, defStyle) { private val binding: ViewFocusedViewBinding by viewBinding() + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + setOnClickListener { } + } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt index 1d0c4e95f8b9..e00f22387477 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt @@ -22,7 +22,6 @@ import com.duckduckgo.anvil.annotations.ContributesActivePlugin import com.duckduckgo.anvil.annotations.ContributesActivePluginPoint import com.duckduckgo.common.utils.plugins.ActivePluginPoint import com.duckduckgo.di.scopes.ActivityScope -import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.newtabpage.api.FocusedViewPlugin import com.duckduckgo.newtabpage.api.FocusedViewVersion import com.squareup.anvil.annotations.ContributesBinding @@ -73,7 +72,7 @@ class FocusedPage @Inject constructor() : FocusedViewPlugin { } @ContributesActivePluginPoint( - scope = AppScope::class, + scope = ActivityScope::class, boundType = FocusedViewPlugin::class, ) private interface FocusedViewPluginPointTrigger diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt index 503fc3ee02bb..f33337320962 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageView.kt @@ -219,7 +219,6 @@ class NewTabLegacyPageView @JvmOverloads constructor( } private fun render(viewState: ViewState) { - Timber.d("New Tab: render $viewState") if (viewState.message == null && viewState.favourites.isEmpty()) { homeBackgroundLogo.showLogo() } else { @@ -348,8 +347,6 @@ class NewTabLegacyPageView @JvmOverloads constructor( newMessage: Boolean, ) { val parentVisible = (this.parent as? View)?.isVisible ?: false - Timber.d("New Tab: RMF isParentVisible $parentVisible") - val shouldRender = parentVisible && (newMessage || binding.messageCta.isGone) if (shouldRender) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt index 47b182ae000a..79be3223253a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabLegacyPageViewModel.kt @@ -32,6 +32,7 @@ import com.duckduckgo.app.browser.viewstate.SavedSiteChangedViewState import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.playstore.PlayStoreUtils import com.duckduckgo.di.scopes.ViewScope @@ -60,7 +61,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber @SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle @ContributesViewModel(ViewScope::class) @@ -111,15 +111,17 @@ class NewTabLegacyPageViewModel @Inject constructor( private var lastRemoteMessageSeen: RemoteMessage? = null val hiddenIds = MutableStateFlow(HiddenBookmarksIds()) + private val _hiddenIds = MutableStateFlow(HiddenBookmarksIds()) + private val _viewState = MutableStateFlow(ViewState()) val viewState = _viewState.asStateFlow() + private val command = Channel(1, BufferOverflow.DROP_OLDEST) internal fun commands(): Flow = command.receiveAsFlow() override fun onStart(owner: LifecycleOwner) { super.onStart(owner) - Timber.d("New Tab: onStart") viewModelScope.launch(dispatchers.io()) { savedSitesRepository.getFavorites() .combine(hiddenIds) { favorites, hiddenIds -> @@ -133,7 +135,6 @@ class NewTabLegacyPageViewModel @Inject constructor( } .flowOn(dispatchers.io()) .onEach { snapshot -> - Timber.d("New Tab: $snapshot") val newMessage = snapshot.remoteMessage?.id != lastRemoteMessageSeen?.id if (newMessage) { lastRemoteMessageSeen = snapshot.remoteMessage @@ -239,7 +240,7 @@ class NewTabLegacyPageViewModel @Inject constructor( viewModelScope.launch(dispatchers.io()) { when (savedSite) { is Bookmark -> { - hiddenIds.emit( + _hiddenIds.emit( hiddenIds.value.copy( bookmarks = hiddenIds.value.bookmarks + savedSite.id, favorites = hiddenIds.value.favorites + savedSite.id, @@ -248,7 +249,7 @@ class NewTabLegacyPageViewModel @Inject constructor( } is Favorite -> { - hiddenIds.emit(hiddenIds.value.copy(favorites = hiddenIds.value.favorites + savedSite.id)) + _hiddenIds.emit(hiddenIds.value.copy(favorites = hiddenIds.value.favorites + savedSite.id)) } } withContext(dispatchers.main()) { @@ -276,7 +277,7 @@ class NewTabLegacyPageViewModel @Inject constructor( fun undoDelete(savedSite: SavedSite) { viewModelScope.launch(dispatchers.io()) { - hiddenIds.emit( + _hiddenIds.emit( hiddenIds.value.copy( favorites = hiddenIds.value.favorites - savedSite.id, bookmarks = hiddenIds.value.bookmarks - savedSite.id, @@ -316,6 +317,7 @@ class NewTabLegacyPageViewModel @Inject constructor( override fun onFavoriteAdded() { pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) + pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY, type = DAILY) } override fun onFavoriteRemoved() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabPageProvider.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabPageProvider.kt index 12b3cf744d4a..5379e5794c2d 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabPageProvider.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabPageProvider.kt @@ -21,6 +21,7 @@ import android.view.View import com.duckduckgo.anvil.annotations.ContributesActivePlugin import com.duckduckgo.anvil.annotations.ContributesActivePluginPoint import com.duckduckgo.common.utils.plugins.ActivePluginPoint +import com.duckduckgo.di.scopes.ActivityScope import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.newtabpage.api.NewTabPagePlugin import com.duckduckgo.newtabpage.api.NewTabPageVersion @@ -34,7 +35,7 @@ interface NewTabPageProvider { fun provideNewTabPageVersion(): Flow } -@ContributesBinding(scope = AppScope::class) +@ContributesBinding(scope = ActivityScope::class) class RealNewTabPageProvider @Inject constructor( private val newTabPageVersions: ActivePluginPoint, ) : NewTabPageProvider { diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 6bb87b439ad8..9bc6f789db68 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -30,6 +30,7 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.FragmentBrowserTabBinding import com.duckduckgo.app.browser.databinding.IncludeOnboardingViewDaxDialogBinding import com.duckduckgo.app.cta.model.CtaId +import com.duckduckgo.app.cta.model.CtaId.DAX_END import com.duckduckgo.app.cta.ui.DaxBubbleCta.DaxDialogIntroOption import com.duckduckgo.app.cta.ui.DaxCta.Companion.MAX_DAYS_ALLOWED import com.duckduckgo.app.global.install.AppInstallStore @@ -41,6 +42,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel.PixelValues.DAX_FIRE_DIALOG_CT import com.duckduckgo.app.trackerdetection.model.Entity import com.duckduckgo.common.ui.view.TypeAnimationTextView import com.duckduckgo.common.ui.view.button.DaxButton +import com.duckduckgo.common.ui.view.button.DaxButtonPrimary import com.duckduckgo.common.ui.view.gone import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.view.text.DaxTextView @@ -499,6 +501,12 @@ sealed class DaxBubbleCta( } } + fun setOnPrimaryCtaClicked(onButtonClicked: () -> Unit) { + ctaView?.findViewById(R.id.primaryCta)?.setOnClickListener { + onButtonClicked.invoke() + } + } + override fun pixelCancelParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) override fun pixelOkParameters(): Map = mapOf(Pixel.PixelParameter.CTA_SHOWN to ctaPixelParam) diff --git a/app/src/main/java/com/duckduckgo/app/downloads/DownloadsActivity.kt b/app/src/main/java/com/duckduckgo/app/downloads/DownloadsActivity.kt index 5cc640a5ec39..66fd03be66ca 100644 --- a/app/src/main/java/com/duckduckgo/app/downloads/DownloadsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/downloads/DownloadsActivity.kt @@ -27,9 +27,11 @@ import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager +import com.duckduckgo.anvil.annotations.ContributeToActivityStarter import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.ActivityDownloadsBinding +import com.duckduckgo.app.downloads.DownloadsScreens.DownloadsScreenNoParams import com.duckduckgo.app.downloads.DownloadsViewModel.Command import com.duckduckgo.app.downloads.DownloadsViewModel.Command.* import com.duckduckgo.app.downloads.DownloadsViewModel.ViewState @@ -51,6 +53,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @InjectWith(ActivityScope::class) +@ContributeToActivityStarter(DownloadsScreenNoParams::class) class DownloadsActivity : DuckDuckGoActivity() { private val viewModel: DownloadsViewModel by bindViewModel() diff --git a/app/src/main/java/com/duckduckgo/app/downloads/DownloadsNewTabShortcutPlugin.kt b/app/src/main/java/com/duckduckgo/app/downloads/DownloadsNewTabShortcutPlugin.kt new file mode 100644 index 000000000000..a33717718c89 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/downloads/DownloadsNewTabShortcutPlugin.kt @@ -0,0 +1,78 @@ +/* + * 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.downloads + +import android.content.Context +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.downloads.DownloadsScreens.DownloadsScreenNoParams +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.newtabpage.api.NewTabPageShortcutPlugin +import com.duckduckgo.newtabpage.api.NewTabShortcut +import javax.inject.Inject + +@ContributesActivePlugin( + AppScope::class, + boundType = NewTabPageShortcutPlugin::class, + priority = 3, +) +class DownloadsNewTabShortcutPlugin @Inject constructor( + private val globalActivityStarter: GlobalActivityStarter, + private val setting: DownloadsNewTabShortcutSetting, +) : NewTabPageShortcutPlugin { + + inner class DownloadsShortcut() : NewTabShortcut { + override fun name(): String = "downloads" + override fun titleResource(): Int = R.string.newTabPageShortcutDownloads + override fun iconResource(): Int = R.drawable.ic_shortcut_downloads + } + + override fun getShortcut(): NewTabShortcut { + return DownloadsShortcut() + } + + override fun onClick(context: Context) { + globalActivityStarter.start(context, DownloadsScreenNoParams) + } + + override suspend fun isUserEnabled(): Boolean { + return setting.self().isEnabled() + } + + override suspend fun setUserEnabled(enabled: Boolean) { + if (enabled) { + setting.self().setEnabled(Toggle.State(true)) + } else { + setting.self().setEnabled(Toggle.State(false)) + } + } +} + +/** + * Local feature/settings - they will never be in remote config + */ +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "downloadsNewTabShortcutSetting", +) +interface DownloadsNewTabShortcutSetting { + @Toggle.DefaultValue(true) + fun self(): Toggle +} diff --git a/app/src/main/java/com/duckduckgo/app/downloads/DownloadsScreens.kt b/app/src/main/java/com/duckduckgo/app/downloads/DownloadsScreens.kt new file mode 100644 index 000000000000..55d3644ae0f1 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/downloads/DownloadsScreens.kt @@ -0,0 +1,29 @@ +/* + * 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.downloads + +import com.duckduckgo.navigation.api.GlobalActivityStarter.ActivityParams + +sealed interface DownloadsScreens { + + /** + * Launch the Downloads activity + */ + object DownloadsScreenNoParams : ActivityParams { + private fun readResolve(): Any = DownloadsScreenNoParams + } +} diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsNewTabShortcutPlugin.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsNewTabShortcutPlugin.kt new file mode 100644 index 000000000000..7e1371bed3ec --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsNewTabShortcutPlugin.kt @@ -0,0 +1,78 @@ +/* + * 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.content.Context +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.app.browser.R +import com.duckduckgo.browser.api.ui.BrowserScreens.SettingsScreenNoParams +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.newtabpage.api.NewTabPageShortcutPlugin +import com.duckduckgo.newtabpage.api.NewTabShortcut +import javax.inject.Inject + +@ContributesActivePlugin( + AppScope::class, + boundType = NewTabPageShortcutPlugin::class, + priority = 4, +) +class SettingsNewTabShortcutPlugin @Inject constructor( + private val globalActivityStarter: GlobalActivityStarter, + private val setting: SettingsNewTabShortcutSetting, +) : NewTabPageShortcutPlugin { + + inner class SettingsShortcut() : NewTabShortcut { + override fun name(): String = "settings" + override fun titleResource(): Int = R.string.newTabPageShortcutSettings + override fun iconResource(): Int = R.drawable.ic_shortcut_settings + } + + override fun getShortcut(): NewTabShortcut { + return SettingsShortcut() + } + + override fun onClick(context: Context) { + globalActivityStarter.start(context, SettingsScreenNoParams) + } + + override suspend fun isUserEnabled(): Boolean { + return setting.self().isEnabled() + } + + override suspend fun setUserEnabled(enabled: Boolean) { + if (enabled) { + setting.self().setEnabled(Toggle.State(true)) + } else { + setting.self().setEnabled(Toggle.State(false)) + } + } +} + +/** + * Local feature/settings - they will never be in remote config + */ +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "settingsNewTabShortcutSetting", +) +interface SettingsNewTabShortcutSetting { + @Toggle.DefaultValue(true) + fun self(): Toggle +} diff --git a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt index 2d287b5a04a5..8c782c26d9fc 100644 --- a/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/systemsearch/SystemSearchViewModel.kt @@ -31,6 +31,7 @@ import com.duckduckgo.app.onboarding.store.isNewUser import com.duckduckgo.app.pixels.AppPixelName.* import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.UpdateVoiceSearch import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.SingleLiveEvent @@ -362,6 +363,7 @@ class SystemSearchViewModel @Inject constructor( override fun onFavoriteAdded() { pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) + pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY, type = DAILY) } override fun onFavoriteRemoved() { diff --git a/app/src/main/res/drawable/ic_shortcut_downloads.xml b/app/src/main/res/drawable/ic_shortcut_downloads.xml new file mode 100644 index 000000000000..8d6461fdd1fd --- /dev/null +++ b/app/src/main/res/drawable/ic_shortcut_downloads.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_shortcut_settings.xml b/app/src/main/res/drawable/ic_shortcut_settings.xml new file mode 100644 index 000000000000..3f2f580a62ec --- /dev/null +++ b/app/src/main/res/drawable/ic_shortcut_settings.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/app/src/main/res/layout/include_new_browser_tab.xml b/app/src/main/res/layout/include_new_browser_tab.xml index 338b9f13d857..d92868e53e1a 100644 --- a/app/src/main/res/layout/include_new_browser_tab.xml +++ b/app/src/main/res/layout/include_new_browser_tab.xml @@ -14,7 +14,7 @@ ~ limitations under the License. --> - + app:layout_constraintStart_toStartOf="parent" /> - + diff --git a/app/src/main/res/layout/include_onboarding_view_dax_dialog.xml b/app/src/main/res/layout/include_onboarding_view_dax_dialog.xml index 85eba25ea3fa..2398de261e92 100644 --- a/app/src/main/res/layout/include_onboarding_view_dax_dialog.xml +++ b/app/src/main/res/layout/include_onboarding_view_dax_dialog.xml @@ -100,9 +100,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/daxDialogButtonSpacing" - android:visibility="invisible" - app:buttonSize="small" - tools:text="Button" /> + android:text="@string/daxDialogHighFive" + android:alpha="0" + tools:alpha="1" + app:buttonSize="small" /> diff --git a/app/src/main/res/layout/view_focused_view.xml b/app/src/main/res/layout/view_focused_view.xml index 8c28991092c4..1a609be51b50 100644 --- a/app/src/main/res/layout/view_focused_view.xml +++ b/app/src/main/res/layout/view_focused_view.xml @@ -14,21 +14,13 @@ ~ limitations under the License. --> - - - - - + app:isExpandable="false" + android:background="?attr/daxColorSurface" /> - diff --git a/app/src/main/res/layout/view_new_tab_legacy.xml b/app/src/main/res/layout/view_new_tab_legacy.xml index 5726c924c509..c041bcf053a0 100644 --- a/app/src/main/res/layout/view_new_tab_legacy.xml +++ b/app/src/main/res/layout/view_new_tab_legacy.xml @@ -22,7 +22,7 @@ android:layout_height="match_parent" android:orientation="vertical"> - Relaunch Now Dismiss + + Downloads + Settings diff --git a/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabPageProviderTest.kt b/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabPageProviderTest.kt new file mode 100644 index 000000000000..4c8c5feef81f --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/browser/newtab/NewTabPageProviderTest.kt @@ -0,0 +1,125 @@ +/* + * 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.browser.newtab + +import android.content.Context +import android.view.View +import app.cash.turbine.test +import com.duckduckgo.common.utils.plugins.ActivePluginPoint +import com.duckduckgo.newtabpage.api.NewTabPagePlugin +import com.duckduckgo.newtabpage.api.NewTabPageVersion +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertTrue +import org.junit.Test + +class NewTabPageProviderTest { + + private lateinit var testee: NewTabPageProvider + + @Test + fun whenLegacyPluginEnabledThenLegacyViewProvided() = runTest { + testee = RealNewTabPageProvider(legacyPluginEnabled) + + testee.provideNewTabPageVersion().test { + expectMostRecentItem().also { + assertTrue(it.name == NewTabPageVersion.LEGACY.name) + } + } + } + + @Test + fun whenNewPluginEnabledThenNewViewProvided() = runTest { + testee = RealNewTabPageProvider(newPluginEnabled) + + testee.provideNewTabPageVersion().test { + expectMostRecentItem().also { + assertTrue(it.name == NewTabPageVersion.NEW.name) + } + } + } + + @Test + fun whenAllPluginsEnabledThenLegacyViewProvided() = runTest { + testee = RealNewTabPageProvider(allPluginsEnabled) + + testee.provideNewTabPageVersion().test { + expectMostRecentItem().also { + assertTrue(it.name == NewTabPageVersion.LEGACY.name) + } + } + } + + @Test + fun whenNoPluginsEnabledThenLegacyViewProvided() = runTest { + testee = RealNewTabPageProvider(noPluginsEnabled) + + testee.provideNewTabPageVersion().test { + expectMostRecentItem().also { + assertTrue(it.name == NewTabPageVersion.LEGACY.name) + } + } + } + + private val legacyPluginEnabled = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + LegacyNewTabPlugin(), + ) + } + } + + private val newPluginEnabled = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + NewNewTabPlugin(), + ) + } + } + + private val allPluginsEnabled = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + LegacyNewTabPlugin(), + NewNewTabPlugin(), + ) + } + } + + private val noPluginsEnabled = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return emptyList() + } + } + + class LegacyNewTabPlugin : NewTabPagePlugin { + override val name: String + get() = NewTabPageVersion.LEGACY.name + + override fun getView(context: Context): View { + return View(context) + } + } + + class NewNewTabPlugin() : NewTabPagePlugin { + override val name: String + get() = NewTabPageVersion.NEW.name + + override fun getView(context: Context): View { + return View(context) + } + } +} diff --git a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt index a7d410bf59d5..4e699d219766 100644 --- a/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt +++ b/autofill/autofill-api/src/main/java/com/duckduckgo/autofill/api/AutofillScreens.kt @@ -55,4 +55,5 @@ enum class AutofillSettingsLaunchSource { BrowserSnackbar, InternalDevSettings, Unknown, + NewTabShortcut, } diff --git a/autofill/autofill-impl/build.gradle b/autofill/autofill-impl/build.gradle index 0fecd2fc4ccd..fd2d19b14a74 100644 --- a/autofill/autofill-impl/build.gradle +++ b/autofill/autofill-impl/build.gradle @@ -41,6 +41,8 @@ dependencies { implementation project(path: ':sync-api') implementation project(path: ':navigation-api') implementation project(':user-agent-api') + implementation project(':new-tab-page-api') + implementation project(':data-store-api') anvil project(path: ':anvil-compiler') implementation project(path: ':anvil-annotations') diff --git a/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/newtab/AutofillNewTabShortcut.kt b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/newtab/AutofillNewTabShortcut.kt new file mode 100644 index 000000000000..a3a8c86cfcc0 --- /dev/null +++ b/autofill/autofill-impl/src/main/java/com/duckduckgo/autofill/impl/newtab/AutofillNewTabShortcut.kt @@ -0,0 +1,79 @@ +/* + * 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.autofill.impl.newtab + +import android.content.Context +import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.autofill.api.AutofillScreens.AutofillSettingsScreen +import com.duckduckgo.autofill.api.AutofillSettingsLaunchSource +import com.duckduckgo.autofill.impl.R +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.navigation.api.GlobalActivityStarter +import com.duckduckgo.newtabpage.api.NewTabPageShortcutPlugin +import com.duckduckgo.newtabpage.api.NewTabShortcut +import javax.inject.Inject + +@ContributesActivePlugin( + AppScope::class, + boundType = NewTabPageShortcutPlugin::class, + priority = 2, +) +class AutofillNewTabShortcutPlugin @Inject constructor( + private val globalActivityStarter: GlobalActivityStarter, + private val setting: AutofillNewTabShortcutSetting, +) : NewTabPageShortcutPlugin { + + inner class PasswordsShortcut() : NewTabShortcut { + override fun name(): String = "passwords" + override fun titleResource(): Int = R.string.newTabPageShortcutPasswords + override fun iconResource(): Int = R.drawable.ic_shortcut_passwords + } + + override fun getShortcut(): NewTabShortcut { + return PasswordsShortcut() + } + + override fun onClick(context: Context) { + globalActivityStarter.start(context, AutofillSettingsScreen(AutofillSettingsLaunchSource.NewTabShortcut)) + } + + override suspend fun isUserEnabled(): Boolean { + return setting.self().isEnabled() + } + + override suspend fun setUserEnabled(enabled: Boolean) { + if (enabled) { + setting.self().setEnabled(Toggle.State(true)) + } else { + setting.self().setEnabled(Toggle.State(false)) + } + } +} + +/** + * Local feature/settings - they will never be in remote config + */ +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "autofillNewTabShortcutSetting", +) +interface AutofillNewTabShortcutSetting { + @Toggle.DefaultValue(true) + fun self(): Toggle +} diff --git a/autofill/autofill-impl/src/main/res/drawable/ic_shortcut_passwords.xml b/autofill/autofill-impl/src/main/res/drawable/ic_shortcut_passwords.xml new file mode 100644 index 000000000000..74da796dff7d --- /dev/null +++ b/autofill/autofill-impl/src/main/res/drawable/ic_shortcut_passwords.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/autofill/autofill-impl/src/main/res/values/donottranslate.xml b/autofill/autofill-impl/src/main/res/values/donottranslate.xml index def6aa352279..633f147275b8 100644 --- a/autofill/autofill-impl/src/main/res/values/donottranslate.xml +++ b/autofill/autofill-impl/src/main/res/values/donottranslate.xml @@ -15,5 +15,6 @@ --> - + + Passwords \ No newline at end of file diff --git a/browser-api/src/main/java/com/duckduckgo/browser/api/ui/BrowserScreens.kt b/browser-api/src/main/java/com/duckduckgo/browser/api/ui/BrowserScreens.kt index 1b505544ba19..1d66364fa589 100644 --- a/browser-api/src/main/java/com/duckduckgo/browser/api/ui/BrowserScreens.kt +++ b/browser-api/src/main/java/com/duckduckgo/browser/api/ui/BrowserScreens.kt @@ -37,4 +37,9 @@ sealed class BrowserScreens { * Use this model to launch the Settings screen */ object SettingsScreenNoParams : GlobalActivityStarter.ActivityParams + + /** + * Use this model to launch the New Tab Settings screen + */ + object NewTabSettingsScreenNoParams : GlobalActivityStarter.ActivityParams } diff --git a/build.gradle b/build.gradle index 9d70c6104ff3..010d105fe758 100644 --- a/build.gradle +++ b/build.gradle @@ -109,8 +109,7 @@ subprojects { // API modules cannot depend on dagger/anvil if (projectPath.endsWith("api") && projectPath != ":feature-toggles-api" - && projectPath != ":settings-api") - { + && projectPath != ":settings-api") { def notAllowedDeps = ["anvil", "dagger"] if (notAllowedDeps.contains(dependency.name)) { throw new GradleException("Invalid dependency $projectPath -> " + diff --git a/code-formatting.gradle b/code-formatting.gradle index 2f44593cc245..09c4dad5f3a9 100644 --- a/code-formatting.gradle +++ b/code-formatting.gradle @@ -4,7 +4,7 @@ apply plugin: 'org.jmailen.kotlinter' spotless { java { target 'src/*/java/**/*.java' - googleJavaFormat('1.8').aosp() + googleJavaFormat('1.22.0').aosp() removeUnusedImports() trimTrailingWhitespace() indentWithSpaces() diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/menu/PopupMenu.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/menu/PopupMenu.kt index 032628978dd3..92a060072244 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/menu/PopupMenu.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/menu/PopupMenu.kt @@ -115,9 +115,7 @@ open class PopupMenu( private const val MARGIN = 16 private const val ELEVATION = 6f - const val POPUP_DEFAULT_ELEVATION_DP = 8f - const val EDGE_TREATMENT_DISTANCE_FROM_EDGE = 200f - const val POPUP_HORIZONTAL_OFFSET_DP = -4 + const val POPUP_DEFAULT_ELEVATION_DP = 4f fun inflate( layoutInflater: LayoutInflater, diff --git a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/ViewExtension.kt b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/ViewExtension.kt index e07c83658d73..99e3290c438a 100644 --- a/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/ViewExtension.kt +++ b/common/common-ui/src/main/java/com/duckduckgo/common/ui/view/ViewExtension.kt @@ -180,6 +180,18 @@ internal inline fun View.updateLayoutParams(block: ViewGroup.LayoutParams.() -> updateLayoutParam(this, block) } +fun View.visibilityChanged(action: (View) -> Unit) { + this.viewTreeObserver.addOnGlobalLayoutListener { + val newVis: Int = this.visibility + if (this.tag as Int? != newVis) { + this.tag = this.visibility + + // visibility has changed + action(this) + } + } +} + /** * Executes [block] with a typed version of the View's layoutParams and reassigns the * layoutParams with the updated version. diff --git a/common/common-ui/src/main/res/drawable/background_circular_icon_container.xml b/common/common-ui/src/main/res/drawable/background_circular_icon_container.xml new file mode 100644 index 000000000000..d8fb5c6ddf7d --- /dev/null +++ b/common/common-ui/src/main/res/drawable/background_circular_icon_container.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/common/common-ui/src/main/res/drawable/background_rounded_icon.xml b/common/common-ui/src/main/res/drawable/background_rounded_icon.xml new file mode 100644 index 000000000000..db403f46d024 --- /dev/null +++ b/common/common-ui/src/main/res/drawable/background_rounded_icon.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/common/common-ui/src/main/res/drawable/background_rounded_surface.xml b/common/common-ui/src/main/res/drawable/background_rounded_surface.xml index 9c07992dc23b..e9784f02063b 100644 --- a/common/common-ui/src/main/res/drawable/background_rounded_surface.xml +++ b/common/common-ui/src/main/res/drawable/background_rounded_surface.xml @@ -14,6 +14,6 @@ ~ limitations under the License. --> - + \ No newline at end of file diff --git a/common/common-ui/src/main/res/drawable/selectable_rounded_icon.xml b/common/common-ui/src/main/res/drawable/selectable_rounded_icon.xml new file mode 100644 index 000000000000..5cb1e65feeda --- /dev/null +++ b/common/common-ui/src/main/res/drawable/selectable_rounded_icon.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/common/common-ui/src/main/res/drawable/selectable_rounded_surface_ripple.xml b/common/common-ui/src/main/res/drawable/selectable_rounded_surface_ripple.xml new file mode 100644 index 000000000000..23921cb81067 --- /dev/null +++ b/common/common-ui/src/main/res/drawable/selectable_rounded_surface_ripple.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/common/common-ui/src/main/res/values/design-system-colors.xml b/common/common-ui/src/main/res/values/design-system-colors.xml index ea497bebc9a1..5801a9aeabf4 100644 --- a/common/common-ui/src/main/res/values/design-system-colors.xml +++ b/common/common-ui/src/main/res/values/design-system-colors.xml @@ -148,7 +148,9 @@ #80CCDAFF #8FABF9 #7295F6 + #337295F6 #3969EF + #333969EF #243969EF #2B55CA #1E42A4 diff --git a/common/common-ui/src/main/res/values/design-system-dimensions.xml b/common/common-ui/src/main/res/values/design-system-dimensions.xml index 7eaacf3b51a0..419f676c1bce 100644 --- a/common/common-ui/src/main/res/values/design-system-dimensions.xml +++ b/common/common-ui/src/main/res/values/design-system-dimensions.xml @@ -68,8 +68,9 @@ 80dp - 64dp + 56dp 32dp + 72dp 280dp diff --git a/common/common-ui/src/main/res/values/design-system-theming.xml b/common/common-ui/src/main/res/values/design-system-theming.xml index c804c5f3951f..de8a74c80137 100644 --- a/common/common-ui/src/main/res/values/design-system-theming.xml +++ b/common/common-ui/src/main/res/values/design-system-theming.xml @@ -76,8 +76,8 @@ @style/Widget.DuckDuckGo.DaxButton.Ghost @style/Widget.DuckDuckGo.DaxButton.DestructiveGhost - @style/Widget.DuckDuckGo.OneLineListItem @style/Widget.DuckDuckGo.TwoLineListItem + @style/Widget.DuckDuckGo.OneLineListItem @style/Widget.DuckDuckGo.CardView @style/Widget.DuckDuckGo.v3.Switch @@ -119,7 +119,6 @@ ?attr/daxColorPrimaryText @color/black60 - @color/blue10 @color/white @color/black84 ?attr/daxColorSurface @@ -162,6 +161,7 @@ @color/yellow50 @color/white12 @color/white + @color/blue30_20 @@ -239,6 +239,7 @@ @color/yellow50 @color/black6 @color/gray85 + @color/blue50_20 diff --git a/common/common-ui/src/main/res/values/widgets.xml b/common/common-ui/src/main/res/values/widgets.xml index cfbe8c3d3902..a8f9d51da970 100644 --- a/common/common-ui/src/main/res/values/widgets.xml +++ b/common/common-ui/src/main/res/values/widgets.xml @@ -200,7 +200,6 @@ @dimen/twoLineItemHeight - @@ -416,13 +418,20 @@ match_parent wrap_content @drawable/background_message_cta - 4dp - @dimen/keyline_2 - @dimen/keyline_2 + 2dp + @dimen/keyline_4 + @dimen/keyline_4 @dimen/keyline_4 @dimen/keyline_4 + + + + \ No newline at end of file diff --git a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/NewTabPageSectionProviderTest.kt b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/NewTabPageSectionProviderTest.kt new file mode 100644 index 000000000000..e8b0ff0d522e --- /dev/null +++ b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/NewTabPageSectionProviderTest.kt @@ -0,0 +1,152 @@ +/* + * 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.newtabpage.impl + +import app.cash.turbine.test +import com.duckduckgo.newtabpage.api.NewTabPageSection +import com.duckduckgo.newtabpage.api.NewTabPageSectionProvider +import com.duckduckgo.newtabpage.impl.settings.NewTabSettingsStore +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class NewTabPageSectionProviderTest { + + private lateinit var testee: NewTabPageSectionProvider + + private var newTabSettingsStore: NewTabSettingsStore = mock() + + @Before + fun setup() { + whenever(newTabSettingsStore.sectionSettings).thenReturn( + listOf( + NewTabPageSection.APP_TRACKING_PROTECTION.name, + NewTabPageSection.FAVOURITES.name, + NewTabPageSection.SHORTCUTS.name, + ), + ) + } + + @Test + fun whenNoSectionsActiveThenNoPluginsReturned() = runTest { + testee = RealNewTabPageSectionProvider( + disabledSectionPlugins, + disabledSectionSettingsPlugins, + newTabSettingsStore, + ) + + testee.provideSections().test { + expectMostRecentItem().also { + assertTrue(it.isEmpty()) + } + } + } + + @Test + fun whenSectionsAllActiveThenPluginsReturnedInOrder() = runTest { + testee = RealNewTabPageSectionProvider( + enabledSectionPlugins, + activeSectionSettingsPlugins, + newTabSettingsStore, + ) + + testee.provideSections().test { + expectMostRecentItem().also { + assertTrue(it[0].name == NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name) + assertTrue(it[1].name == NewTabPageSection.APP_TRACKING_PROTECTION.name) + assertTrue(it[2].name == NewTabPageSection.FAVOURITES.name) + assertTrue(it[3].name == NewTabPageSection.SHORTCUTS.name) + } + } + } + + @Test + fun whenUserDisabledFavoritesThenPluginsReturnedInOrder() = runTest { + whenever(newTabSettingsStore.sectionSettings).thenReturn( + listOf( + NewTabPageSection.APP_TRACKING_PROTECTION.name, + NewTabPageSection.SHORTCUTS.name, + ), + ) + + testee = RealNewTabPageSectionProvider( + enabledSectionPlugins, + activeSectionSettingsPlugins, + newTabSettingsStore, + ) + + testee.provideSections().test { + expectMostRecentItem().also { + assertTrue(it[0].name == NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name) + assertTrue(it[1].name == NewTabPageSection.APP_TRACKING_PROTECTION.name) + assertTrue(it[2].name == NewTabPageSection.SHORTCUTS.name) + } + } + } + + @Test + fun whenRemoteDisabledFavoritesThenPluginsReturnedInOrder() = runTest { + whenever(newTabSettingsStore.sectionSettings).thenReturn( + listOf( + NewTabPageSection.APP_TRACKING_PROTECTION.name, + NewTabPageSection.FAVOURITES.name, + NewTabPageSection.SHORTCUTS.name, + ), + ) + + testee = RealNewTabPageSectionProvider( + favoriteDisabledSectionPlugins, + activeSectionSettingsPlugins, + newTabSettingsStore, + ) + + testee.provideSections().test { + expectMostRecentItem().also { + assertTrue(it[0].name == NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name) + assertTrue(it[1].name == NewTabPageSection.APP_TRACKING_PROTECTION.name) + assertTrue(it[2].name == NewTabPageSection.SHORTCUTS.name) + } + } + } + + @Test + fun whenDisabledFavoritesSettingThenPluginsReturnedInOrder() = runTest { + whenever(newTabSettingsStore.sectionSettings).thenReturn( + listOf( + NewTabPageSection.APP_TRACKING_PROTECTION.name, + NewTabPageSection.SHORTCUTS.name, + ), + ) + + testee = RealNewTabPageSectionProvider( + enabledSectionsPlugins, + activeSectionSettingsPlugins, + newTabSettingsStore, + ) + + testee.provideSections().test { + expectMostRecentItem().also { + assertTrue(it[0].name == NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name) + assertTrue(it[1].name == NewTabPageSection.APP_TRACKING_PROTECTION.name) + assertTrue(it[2].name == NewTabPageSection.SHORTCUTS.name) + } + } + } +} diff --git a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/TestPluginPoints.kt b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/TestPluginPoints.kt new file mode 100644 index 000000000000..2131acc99da4 --- /dev/null +++ b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/TestPluginPoints.kt @@ -0,0 +1,272 @@ +/* + * 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.newtabpage.impl + +import android.content.Context +import android.view.View +import com.duckduckgo.common.utils.plugins.ActivePluginPoint +import com.duckduckgo.common.utils.plugins.PluginPoint +import com.duckduckgo.newtabpage.api.NewTabPageSection +import com.duckduckgo.newtabpage.api.NewTabPageSectionPlugin +import com.duckduckgo.newtabpage.api.NewTabPageSectionSettingsPlugin +import com.duckduckgo.newtabpage.api.NewTabPageShortcutPlugin +import com.duckduckgo.newtabpage.api.NewTabShortcut +import com.duckduckgo.newtabpage.impl.settings.NewTabSettingsStore +import com.duckduckgo.newtabpage.impl.shortcuts.NewTabShortcutDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +val enabledShortcutPlugins = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + FakeShortcutPlugin(FakeShortcut("bookmarks")), + FakeShortcutPlugin(FakeShortcut("chat")), + ) + } +} + +val disabledShortcutPlugins = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return emptyList() + } +} + +class FakeShortcutPluginPoint : PluginPoint { + override fun getPlugins(): List { + return listOf(FakeShortcutPlugin(FakeShortcut("bookmarks")), FakeShortcutPlugin(FakeShortcut("chat"))) + } +} + +class FakeShortcutPlugin(val fakeShortcut: NewTabShortcut) : NewTabPageShortcutPlugin { + + private var enabled: Boolean = true + + override fun getShortcut(): NewTabShortcut { + return fakeShortcut + } + + override fun onClick( + context: Context, + ) { + // no - op + } + + override suspend fun isUserEnabled(): Boolean { + return enabled + } + + override suspend fun setUserEnabled(state: Boolean) { + enabled = state + } +} + +class FakeShortcut(val name: String) : NewTabShortcut { + override fun name(): String { + return name + } + + override fun titleResource(): Int { + return 10 + } + + override fun iconResource(): Int { + return 10 + } +} + +val enabledSectionsPlugins = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + FakeSectionPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK), + FakeSectionPlugin(NewTabPageSection.APP_TRACKING_PROTECTION), + FakeSectionPlugin(NewTabPageSection.FAVOURITES), + FakeSectionPlugin(NewTabPageSection.SHORTCUTS), + ) + } +} + +class FakeSectionPlugin(val section: NewTabPageSection) : NewTabPageSectionPlugin { + private var enabled: Boolean = true + + override val name: String + get() = section.name + + override fun getView(context: Context): View? { + return null + } + + override suspend fun isUserEnabled(): Boolean { + return enabled + } +} + +val enabledSectionSettingsPlugins = listOf( + FakeSectionSettingPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK, true), + FakeSectionSettingPlugin(NewTabPageSection.APP_TRACKING_PROTECTION, true), + FakeSectionSettingPlugin(NewTabPageSection.FAVOURITES, true), + FakeSectionSettingPlugin(NewTabPageSection.SHORTCUTS, true), +) + +class FakeSectionSettingPlugin( + val section: NewTabPageSection, + val active: Boolean, +) : NewTabPageSectionSettingsPlugin { + override val name: String + get() = section.name + + override fun getView(context: Context): View? { + return null + } + + override suspend fun isActive(): Boolean { + return active + } +} + +var allSectionSettings: List = listOf( + NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, + NewTabPageSection.APP_TRACKING_PROTECTION.name, + NewTabPageSection.FAVOURITES.name, + NewTabPageSection.SHORTCUTS.name, +) +var allShortcutSettings: List = listOf( + FakeShortcut("bookmarks").name, + FakeShortcut("passwords").name, + FakeShortcut("chat").name, + FakeShortcut("downloads").name, + FakeShortcut("settings").name, +) + +val activeSectionSettingsPlugins = object : PluginPoint { + override fun getPlugins(): Collection { + return listOf( + FakeActiveSectionSettingPlugin(NewTabPageSection.APP_TRACKING_PROTECTION.name, true), + FakeActiveSectionSettingPlugin(NewTabPageSection.FAVOURITES.name, true), + FakeActiveSectionSettingPlugin(NewTabPageSection.SHORTCUTS.name, true), + ) + } +} + +val disabledSectionSettingsPlugins = object : PluginPoint { + override fun getPlugins(): Collection { + return listOf( + FakeActiveSectionSettingPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, false), + FakeActiveSectionSettingPlugin(NewTabPageSection.APP_TRACKING_PROTECTION.name, false), + FakeActiveSectionSettingPlugin(NewTabPageSection.FAVOURITES.name, false), + FakeActiveSectionSettingPlugin(NewTabPageSection.SHORTCUTS.name, false), + ) + } +} + +private class FakeActiveSectionSettingPlugin( + val section: String, + val isEnabled: Boolean, +) : NewTabPageSectionSettingsPlugin { + override val name: String + get() = section + + override fun getView(context: Context): View? { + return null + } + + override suspend fun isActive(): Boolean { + return isEnabled + } +} + +val enabledSectionPlugins = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + FakeEnabledSectionPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.APP_TRACKING_PROTECTION.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.FAVOURITES.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.SHORTCUTS.name, true), + ) + } +} + +val favoriteDisabledSectionPlugins = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + FakeEnabledSectionPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.APP_TRACKING_PROTECTION.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.FAVOURITES.name, false), + FakeEnabledSectionPlugin(NewTabPageSection.SHORTCUTS.name, true), + ) + } +} + +val disabledSectionPlugins = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + FakeEnabledSectionPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, false), + FakeEnabledSectionPlugin(NewTabPageSection.APP_TRACKING_PROTECTION.name, false), + FakeEnabledSectionPlugin(NewTabPageSection.FAVOURITES.name, false), + FakeEnabledSectionPlugin(NewTabPageSection.SHORTCUTS.name, false), + ) + } +} + +class FakeSettingStore( + sections: List = allSectionSettings, + shortcuts: List = allShortcutSettings, +) : NewTabSettingsStore { + private var fakeSectionSettings: List = sections + private var fakeShortcutSettings: List = shortcuts + + override var sectionSettings: List + get() = fakeSectionSettings + set(value) { + fakeSectionSettings = value + } + override var shortcutSettings: List + get() = fakeShortcutSettings + set(value) { + fakeShortcutSettings = value + } +} + +class FakeEnabledSectionPlugin( + val section: String, + val isUserEnabled: Boolean, +) : NewTabPageSectionPlugin { + override val name: String + get() = section + + override fun getView(context: Context): View? { + return null + } + + override suspend fun isUserEnabled(): Boolean { + return isUserEnabled + } +} + +class FakeShortcutDataStore(enabled: Boolean = false) : NewTabShortcutDataStore { + private var fakeEnabledSetting: Boolean = enabled + + override val isEnabled: Flow + get() = flowOf(fakeEnabledSetting) + + override suspend fun setIsEnabled(enabled: Boolean) { + fakeEnabledSetting = enabled + } + + override suspend fun isEnabled(): Boolean { + return fakeEnabledSetting + } +} diff --git a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/pixels/RealNewTabPixelsTest.kt b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/pixels/RealNewTabPixelsTest.kt new file mode 100644 index 000000000000..5f5faad6313d --- /dev/null +++ b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/pixels/RealNewTabPixelsTest.kt @@ -0,0 +1,238 @@ +package com.duckduckgo.newtabpage.impl.pixels + +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.newtabpage.impl.disabledSectionPlugins +import com.duckduckgo.newtabpage.impl.enabledSectionsPlugins +import com.duckduckgo.savedsites.api.SavedSitesRepository +import kotlinx.coroutines.test.TestScope +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class RealNewTabPixelsTest { + + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private lateinit var testee: RealNewTabPixels + + private val pixel: Pixel = mock() + private val savedSitesRepository: SavedSitesRepository = mock() + + @Before + fun setup() { + testee = RealNewTabPixels( + pixel, + enabledSectionsPlugins, + savedSitesRepository, + TestScope(), + coroutinesTestRule.testDispatcherProvider, + ) + } + + @Test + fun whenFireCustomizePagePressedPixelThenPixelFired() { + testee.fireCustomizePagePressedPixel() + + verify(pixel).fire(NewTabPixelNames.CUSTOMIZE_PAGE_PRESSED) + } + + @Test + fun whenFireShortcutPressedThenPixelFired() { + testee.fireShortcutPressed("shortcut") + + verify(pixel).fire(NewTabPixelNames.SHORTCUT_PRESSED.pixelName + "shortcut") + } + + @Test + fun whenFireShortcutAddedThenPixelFired() { + testee.fireShortcutAdded("shortcut") + + verify(pixel).fire(NewTabPixelNames.SHORTCUT_ADDED.pixelName + "shortcut") + } + + @Test + fun whenFireShortcutRemovedThenPixelFired() { + testee.fireShortcutRemoved("shortcut") + + verify(pixel).fire(NewTabPixelNames.SHORTCUT_REMOVED.pixelName + "shortcut") + } + + @Test + fun whenFireShortcutSectionToggledEnabledThenPixelFired() { + testee.fireShortcutSectionToggled(true) + + verify(pixel).fire(NewTabPixelNames.SHORTCUT_SECTION_TOGGLED_ON) + } + + @Test + fun whenFireShortcutSectionToggledDisabledThenPixelFired() { + testee.fireShortcutSectionToggled(false) + + verify(pixel).fire(NewTabPixelNames.SHORTCUT_SECTION_TOGGLED_OFF) + } + + @Test + fun whenFireSectionReorderedThenPixelFired() { + testee.fireSectionReordered() + + verify(pixel).fire(NewTabPixelNames.SECTION_REARRANGED) + } + + @Test + fun whenNewTabDisplayedAndNoFavoritesThenPixelFired() { + whenever(savedSitesRepository.favoritesCount()).thenReturn(0) + val paramsMap = mutableMapOf().apply { + put(NewTabPixelParameters.FAVORITES, "1") + put(NewTabPixelParameters.SHORTCUTS, "1") + put(NewTabPixelParameters.APP_TRACKING_PROTECTION, "1") + put(NewTabPixelParameters.FAVORITES_COUNT, "0") + } + + testee.fireNewTabDisplayed() + + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED) + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED_UNIQUE, type = DAILY, parameters = paramsMap) + } + + @Test + fun whenNewTabDisplayedAnd1FavoriteThenPixelFired() { + whenever(savedSitesRepository.favoritesCount()).thenReturn(1) + val paramsMap = mutableMapOf().apply { + put(NewTabPixelParameters.FAVORITES, "1") + put(NewTabPixelParameters.SHORTCUTS, "1") + put(NewTabPixelParameters.APP_TRACKING_PROTECTION, "1") + put(NewTabPixelParameters.FAVORITES_COUNT, "1") + } + + testee.fireNewTabDisplayed() + + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED) + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED_UNIQUE, type = DAILY, parameters = paramsMap) + } + + @Test + fun whenNewTabDisplayedAnd3FavoritesThenPixelFired() { + whenever(savedSitesRepository.favoritesCount()).thenReturn(3) + val paramsMap = mutableMapOf().apply { + put(NewTabPixelParameters.FAVORITES, "1") + put(NewTabPixelParameters.SHORTCUTS, "1") + put(NewTabPixelParameters.APP_TRACKING_PROTECTION, "1") + put(NewTabPixelParameters.FAVORITES_COUNT, NewTabPixelValues.FAVORITES_2_3) + } + + testee.fireNewTabDisplayed() + + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED) + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED_UNIQUE, type = DAILY, parameters = paramsMap) + } + + @Test + fun whenNewTabDisplayedAnd5FavoritesThenPixelFired() { + whenever(savedSitesRepository.favoritesCount()).thenReturn(5) + val paramsMap = mutableMapOf().apply { + put(NewTabPixelParameters.FAVORITES, "1") + put(NewTabPixelParameters.SHORTCUTS, "1") + put(NewTabPixelParameters.APP_TRACKING_PROTECTION, "1") + put(NewTabPixelParameters.FAVORITES_COUNT, NewTabPixelValues.FAVORITES_4_5) + } + + testee.fireNewTabDisplayed() + + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED) + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED_UNIQUE, type = DAILY, parameters = paramsMap) + } + + @Test + fun whenNewTabDisplayedAnd10FavoritesThenPixelFired() { + whenever(savedSitesRepository.favoritesCount()).thenReturn(10) + val paramsMap = mutableMapOf().apply { + put(NewTabPixelParameters.FAVORITES, "1") + put(NewTabPixelParameters.SHORTCUTS, "1") + put(NewTabPixelParameters.APP_TRACKING_PROTECTION, "1") + put(NewTabPixelParameters.FAVORITES_COUNT, NewTabPixelValues.FAVORITES_6_10) + } + + testee.fireNewTabDisplayed() + + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED) + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED_UNIQUE, type = DAILY, parameters = paramsMap) + } + + @Test + fun whenNewTabDisplayedAnd15FavoritesThenPixelFired() { + whenever(savedSitesRepository.favoritesCount()).thenReturn(15) + val paramsMap = mutableMapOf().apply { + put(NewTabPixelParameters.FAVORITES, "1") + put(NewTabPixelParameters.SHORTCUTS, "1") + put(NewTabPixelParameters.APP_TRACKING_PROTECTION, "1") + put(NewTabPixelParameters.FAVORITES_COUNT, NewTabPixelValues.FAVORITES_11_15) + } + + testee.fireNewTabDisplayed() + + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED) + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED_UNIQUE, type = DAILY, parameters = paramsMap) + } + + @Test + fun whenNewTabDisplayedAnd20FavoritesThenPixelFired() { + whenever(savedSitesRepository.favoritesCount()).thenReturn(20) + val paramsMap = mutableMapOf().apply { + put(NewTabPixelParameters.FAVORITES, "1") + put(NewTabPixelParameters.SHORTCUTS, "1") + put(NewTabPixelParameters.APP_TRACKING_PROTECTION, "1") + put(NewTabPixelParameters.FAVORITES_COUNT, NewTabPixelValues.FAVORITES_16_25) + } + + testee.fireNewTabDisplayed() + + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED) + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED_UNIQUE, type = DAILY, parameters = paramsMap) + } + + @Test + fun whenNewTabDisplayedAnd50FavoritesThenPixelFired() { + whenever(savedSitesRepository.favoritesCount()).thenReturn(50) + val paramsMap = mutableMapOf().apply { + put(NewTabPixelParameters.FAVORITES, "1") + put(NewTabPixelParameters.SHORTCUTS, "1") + put(NewTabPixelParameters.APP_TRACKING_PROTECTION, "1") + put(NewTabPixelParameters.FAVORITES_COUNT, NewTabPixelValues.FAVORITES_25) + } + + testee.fireNewTabDisplayed() + + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED) + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED_UNIQUE, type = DAILY, parameters = paramsMap) + } + + @Test + fun whenNewTabDisplayedAndNoSectionsEnabledThenPixelFired() { + testee = RealNewTabPixels( + pixel, + disabledSectionPlugins, + savedSitesRepository, + TestScope(), + coroutinesTestRule.testDispatcherProvider, + ) + + whenever(savedSitesRepository.favoritesCount()).thenReturn(50) + val paramsMap = mutableMapOf().apply { + put(NewTabPixelParameters.FAVORITES, "0") + put(NewTabPixelParameters.SHORTCUTS, "0") + put(NewTabPixelParameters.APP_TRACKING_PROTECTION, "0") + put(NewTabPixelParameters.FAVORITES_COUNT, NewTabPixelValues.FAVORITES_25) + } + + testee.fireNewTabDisplayed() + + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED) + verify(pixel).fire(NewTabPixelNames.NEW_TAB_DISPLAYED_UNIQUE, type = DAILY, parameters = paramsMap) + } +} diff --git a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/NewTabPageViewModelTest.kt b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/NewTabPageViewModelTest.kt new file mode 100644 index 000000000000..b10ac4b8da21 --- /dev/null +++ b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/NewTabPageViewModelTest.kt @@ -0,0 +1,160 @@ +/* + * 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.newtabpage.impl.settings + +import androidx.lifecycle.LifecycleOwner +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.newtabpage.api.NewTabPageSection +import com.duckduckgo.newtabpage.api.NewTabPageSectionProvider +import com.duckduckgo.newtabpage.impl.FakeEnabledSectionPlugin +import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels +import com.duckduckgo.newtabpage.impl.view.NewTabPageViewModel +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class NewTabPageViewModelTest { + + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private val sectionProvider: NewTabPageSectionProvider = mock() + private val pixels: NewTabPixels = mock() + private val lifecycleOwner: LifecycleOwner = mock() + + private lateinit var testee: NewTabPageViewModel + + @Before + fun setup() { + testee = NewTabPageViewModel(sectionProvider, pixels, coroutinesTestRule.testDispatcherProvider) + } + + @Test + fun whenViewModelStartsThenCorrectStateEmitted() = runTest { + whenever(sectionProvider.provideSections()).thenReturn(flowOf(emptyList())) + testee.onResume(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.sections.isEmpty()) + assertFalse(it.loading) + assertTrue(it.showDax) + } + } + } + + @Test + fun whenFavouritesShownThenDaxNotVisible() = runTest { + whenever(sectionProvider.provideSections()).thenReturn( + flowOf( + listOf( + FakeEnabledSectionPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.APP_TRACKING_PROTECTION.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.FAVOURITES.name, true), + ), + ), + ) + testee.onResume(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.sections.size == 3) + assertFalse(it.loading) + assertFalse(it.showDax) + } + } + } + + @Test + fun whenShortcutsShownThenDaxNotVisible() = runTest { + whenever(sectionProvider.provideSections()).thenReturn( + flowOf( + listOf( + FakeEnabledSectionPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.APP_TRACKING_PROTECTION.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.SHORTCUTS.name, true), + ), + ), + ) + testee.onResume(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.sections.size == 3) + assertFalse(it.loading) + assertFalse(it.showDax) + } + } + } + + @Test + fun whenShortcutsOrFavouritesNotShownThenDaxVisible() = runTest { + whenever(sectionProvider.provideSections()).thenReturn( + flowOf( + listOf( + FakeEnabledSectionPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.APP_TRACKING_PROTECTION.name, true), + ), + ), + ) + testee.onResume(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.sections.size == 2) + assertFalse(it.loading) + assertTrue(it.showDax) + } + } + } + + @Test + fun whenSectionsProvidedThenCorrectStateEmitted() = runTest { + whenever(sectionProvider.provideSections()).thenReturn( + flowOf( + listOf( + FakeEnabledSectionPlugin(NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.APP_TRACKING_PROTECTION.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.FAVOURITES.name, true), + FakeEnabledSectionPlugin(NewTabPageSection.SHORTCUTS.name, true), + ), + ), + ) + + testee.onResume(lifecycleOwner) + + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.sections.isNotEmpty()) + assertTrue(it.sections.size == 4) + assertFalse(it.loading) + assertFalse(it.showDax) + } + } + } + + @Test + fun whenCustomisePageClickedThenPixelSent() { + testee.onCustomisePageClicked() + + verify(pixels).fireCustomizePagePressedPixel() + } +} diff --git a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/NewTabSetingsViewModelTest.kt b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/NewTabSetingsViewModelTest.kt new file mode 100644 index 000000000000..a74b91819515 --- /dev/null +++ b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/NewTabSetingsViewModelTest.kt @@ -0,0 +1,167 @@ +/* + * 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.newtabpage.impl.settings + +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.newtabpage.api.NewTabPageSection +import com.duckduckgo.newtabpage.impl.FakeSettingStore +import com.duckduckgo.newtabpage.impl.FakeShortcut +import com.duckduckgo.newtabpage.impl.FakeShortcutDataStore +import com.duckduckgo.newtabpage.impl.FakeShortcutPlugin +import com.duckduckgo.newtabpage.impl.enabledSectionSettingsPlugins +import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels +import com.duckduckgo.newtabpage.impl.shortcuts.NewTabShortcutsProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class NewTabSetingsViewModelTest { + + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private val sectionSettingsProvider: NewTabPageSectionSettingsProvider = mock() + private val shortcutsProvider: NewTabShortcutsProvider = mock() + private val pixels: NewTabPixels = mock() + private val shortcutStore = FakeShortcutDataStore() + private val settingsStore = FakeSettingStore() + + private lateinit var testee: NewTabSettingsViewModel + + @Before + fun setup() { + testee = NewTabSettingsViewModel( + sectionSettingsProvider, + shortcutsProvider, + shortcutStore, + settingsStore, + pixels, + coroutinesTestRule.testDispatcherProvider, + ) + } + + @Test + fun whenViewModelStartsThenCorrectStateEmitted() = runTest { + whenever(sectionSettingsProvider.provideSections()).thenReturn(flowOf(emptyList())) + whenever(shortcutsProvider.provideAllShortcuts()).thenReturn(flowOf(emptyList())) + shortcutStore.setIsEnabled(false) + + testee.viewState().test { + expectMostRecentItem().also { + assertTrue(it.sections.isEmpty()) + assertTrue(it.shortcuts.isEmpty()) + assertFalse(it.shortcutsManagementEnabled) + } + } + } + + @Test + fun whenDataIsProvidedThenCorrectStateEmitted() = runTest { + whenever(sectionSettingsProvider.provideSections()).thenReturn(flowOf(enabledSectionSettingsPlugins)) + whenever(shortcutsProvider.provideAllShortcuts()).thenReturn(whenAllShortcutsAvailable()) + shortcutStore.setIsEnabled(true) + + testee.viewState().test { + expectMostRecentItem().also { + assertFalse(it.sections.isEmpty()) + assertTrue(it.sections.size == 4) + assertFalse(it.shortcuts.isEmpty()) + assertTrue(it.shortcuts.size == 5) + assertTrue(it.shortcutsManagementEnabled) + } + } + } + + @Test + fun whenShortcutUnselectedThenSettingsUpdated() = runTest { + whenever(sectionSettingsProvider.provideSections()).thenReturn(flowOf(enabledSectionSettingsPlugins)) + whenever(shortcutsProvider.provideAllShortcuts()).thenReturn(whenAllShortcutsAvailable()) + shortcutStore.setIsEnabled(true) + + val shortcut = ManageShortcutItem(FakeShortcutPlugin(FakeShortcut("bookmarks")), true) + + assertTrue(settingsStore.shortcutSettings.size == 5) + + testee.onShortcutSelected(shortcut) + + val shortcuts = settingsStore.shortcutSettings + assertTrue(shortcuts.size == 4) + } + + @Test + fun whenShortcutSelectedThenSettingsUpdated() = runTest { + whenever(sectionSettingsProvider.provideSections()).thenReturn(flowOf(enabledSectionSettingsPlugins)) + whenever(shortcutsProvider.provideAllShortcuts()).thenReturn(whenAllShortcutsAvailable()) + shortcutStore.setIsEnabled(true) + + val selectedShortcut = ManageShortcutItem(FakeShortcutPlugin(FakeShortcut("newshortcut")), false) + + assertTrue(settingsStore.shortcutSettings.size == 5) + + testee.onShortcutSelected(selectedShortcut) + + val shortcuts = settingsStore.shortcutSettings + assertTrue(shortcuts.size == 6) + } + + @Test + fun whenSectionsSwappedThenStoreUpdate() = runTest { + whenever(sectionSettingsProvider.provideSections()).thenReturn(flowOf(enabledSectionSettingsPlugins)) + whenever(shortcutsProvider.provideAllShortcuts()).thenReturn(whenAllShortcutsAvailable()) + shortcutStore.setIsEnabled(true) + + assertTrue( + settingsStore.sectionSettings == listOf( + NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, + NewTabPageSection.APP_TRACKING_PROTECTION.name, + NewTabPageSection.FAVOURITES.name, + NewTabPageSection.SHORTCUTS.name, + ), + ) + + testee.onSectionsSwapped(1, 0) + + assertTrue( + settingsStore.sectionSettings == listOf( + NewTabPageSection.APP_TRACKING_PROTECTION.name, + NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name, + NewTabPageSection.FAVOURITES.name, + NewTabPageSection.SHORTCUTS.name, + ), + ) + } + + private fun whenAllShortcutsAvailable(): Flow> { + return flowOf( + listOf( + ManageShortcutItem(FakeShortcutPlugin(FakeShortcut("bookmarks")), true), + ManageShortcutItem(FakeShortcutPlugin(FakeShortcut("passwords")), true), + ManageShortcutItem(FakeShortcutPlugin(FakeShortcut("chat")), true), + ManageShortcutItem(FakeShortcutPlugin(FakeShortcut("downloads")), true), + ManageShortcutItem(FakeShortcutPlugin(FakeShortcut("settings")), true), + ), + ) + } +} diff --git a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/RealNewTabPageSectionSettingsProviderTest.kt b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/RealNewTabPageSectionSettingsProviderTest.kt new file mode 100644 index 000000000000..f818c91ce08d --- /dev/null +++ b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/settings/RealNewTabPageSectionSettingsProviderTest.kt @@ -0,0 +1,132 @@ +package com.duckduckgo.newtabpage.impl.settings + +import android.content.Context +import android.view.View +import app.cash.turbine.test +import com.duckduckgo.common.utils.plugins.ActivePluginPoint +import com.duckduckgo.common.utils.plugins.PluginPoint +import com.duckduckgo.newtabpage.api.NewTabPageSection.APP_TRACKING_PROTECTION +import com.duckduckgo.newtabpage.api.NewTabPageSection.FAVOURITES +import com.duckduckgo.newtabpage.api.NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK +import com.duckduckgo.newtabpage.api.NewTabPageSection.SHORTCUTS +import com.duckduckgo.newtabpage.api.NewTabPageSectionPlugin +import com.duckduckgo.newtabpage.api.NewTabPageSectionSettingsPlugin +import com.duckduckgo.newtabpage.impl.FakeEnabledSectionPlugin +import com.duckduckgo.newtabpage.impl.FakeSettingStore +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class RealNewTabPageSectionSettingsProviderTest { + + private lateinit var testee: RealNewTabPageSectionSettingsProvider + private val settingsStore = FakeSettingStore() + + @Before + fun setup() { + testee = RealNewTabPageSectionSettingsProvider(enabledSectionSettingsPlugins, enabledSectionPlugins, settingsStore) + } + + @Test + fun whenAllSectionsEnabledThenSectionsProvided() = runTest { + testee.provideSections().test { + expectMostRecentItem().also { + assertTrue(it.size == 4) + assertTrue(it[0].name == REMOTE_MESSAGING_FRAMEWORK.name) + assertTrue(it[1].name == APP_TRACKING_PROTECTION.name) + assertTrue(it[2].name == FAVOURITES.name) + assertTrue(it[3].name == SHORTCUTS.name) + } + } + } + + @Test + fun whenUserDisabledAllPossibleSectionsThenSectionsProvided() = runTest { + val settingsStore = FakeSettingStore(sections = listOf(REMOTE_MESSAGING_FRAMEWORK.name), emptyList()) + testee = RealNewTabPageSectionSettingsProvider(enabledSectionSettingsPlugins, enabledSectionPlugins, settingsStore) + + testee.provideSections().test { + expectMostRecentItem().also { + assertTrue(it.size == 1) + assertTrue(it[0].name == REMOTE_MESSAGING_FRAMEWORK.name) + } + } + } + + @Test + fun whenRemoteSectionsAreDisabledThenSectionsProvided() = runTest { + val settingsStore = FakeSettingStore( + sections = listOf( + REMOTE_MESSAGING_FRAMEWORK.name, + APP_TRACKING_PROTECTION.name, + FAVOURITES.name, + SHORTCUTS.name, + ), + emptyList(), + ) + + testee = RealNewTabPageSectionSettingsProvider(enabledSectionSettingsPlugins, disabledSectionPlugins, settingsStore) + + testee.provideSections().test { + expectMostRecentItem().also { + assertTrue(it.isEmpty()) + } + } + } + + private val enabledSectionSettingsPlugins = object : PluginPoint { + override fun getPlugins(): Collection { + return listOf( + FakeActiveSectionSettingPlugin(REMOTE_MESSAGING_FRAMEWORK.name, true), + FakeActiveSectionSettingPlugin(APP_TRACKING_PROTECTION.name, true), + FakeActiveSectionSettingPlugin(FAVOURITES.name, true), + FakeActiveSectionSettingPlugin(SHORTCUTS.name, true), + ) + } + } + + private val enabledSectionPlugins = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + FakeEnabledSectionPlugin(REMOTE_MESSAGING_FRAMEWORK.name, true), + FakeEnabledSectionPlugin(APP_TRACKING_PROTECTION.name, true), + FakeEnabledSectionPlugin(FAVOURITES.name, true), + FakeEnabledSectionPlugin(SHORTCUTS.name, true), + ) + } + } + + private val userDisabledSectionPlugins = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return listOf( + FakeEnabledSectionPlugin(REMOTE_MESSAGING_FRAMEWORK.name, false), + FakeEnabledSectionPlugin(APP_TRACKING_PROTECTION.name, false), + FakeEnabledSectionPlugin(FAVOURITES.name, false), + FakeEnabledSectionPlugin(SHORTCUTS.name, false), + ) + } + } + + private val disabledSectionPlugins = object : ActivePluginPoint { + override suspend fun getPlugins(): Collection { + return emptyList() + } + } + + private class FakeActiveSectionSettingPlugin( + val section: String, + val isEnabled: Boolean, + ) : NewTabPageSectionSettingsPlugin { + override val name: String + get() = section + + override fun getView(context: Context): View? { + return null + } + + override suspend fun isActive(): Boolean { + return isEnabled + } + } +} diff --git a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/NewTabShortcutsProviderTest.kt b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/NewTabShortcutsProviderTest.kt new file mode 100644 index 000000000000..3790e3736798 --- /dev/null +++ b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/NewTabShortcutsProviderTest.kt @@ -0,0 +1,57 @@ +/* + * 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.newtabpage.impl.shortcuts + +import app.cash.turbine.test +import com.duckduckgo.newtabpage.impl.FakeSettingStore +import com.duckduckgo.newtabpage.impl.disabledShortcutPlugins +import com.duckduckgo.newtabpage.impl.enabledShortcutPlugins +import com.duckduckgo.newtabpage.impl.settings.NewTabSettingsStore +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.kotlin.mock + +class NewTabShortcutsProviderTest { + + private val newTabSettingsStore: NewTabSettingsStore = mock() + + private lateinit var testee: NewTabShortcutsProvider + + @Test + fun whenShortcutPluginsEnabledThenProvided() = runTest { + val store = FakeSettingStore() + testee = RealNewTabPageShortcutProvider(enabledShortcutPlugins, store) + + testee.provideActiveShortcuts().test { + expectMostRecentItem().also { + assertTrue(it[0].getShortcut().name() == "bookmarks") + assertTrue(it[1].getShortcut().name() == "chat") + } + } + } + + @Test + fun whenShortcutsDisabledThenProvided() = runTest { + testee = RealNewTabPageShortcutProvider(disabledShortcutPlugins, newTabSettingsStore) + testee.provideActiveShortcuts().test { + expectMostRecentItem().also { + assertTrue(it.isEmpty()) + } + } + } +} diff --git a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsNewTabSettingsViewModelTest.kt b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsNewTabSettingsViewModelTest.kt new file mode 100644 index 000000000000..37ef09bbe7c9 --- /dev/null +++ b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsNewTabSettingsViewModelTest.kt @@ -0,0 +1,68 @@ +package com.duckduckgo.newtabpage.impl.shortcuts + +import androidx.lifecycle.LifecycleOwner +import app.cash.turbine.test +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class ShortcutsNewTabSettingsViewModelTest { + + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private lateinit var testee: ShortcutsNewTabSettingsViewModel + private val setting: NewTabShortcutDataStore = mock() + private val lifecycleOwner: LifecycleOwner = mock() + private val pixels: NewTabPixels = mock() + + @Before + fun setup() { + testee = ShortcutsNewTabSettingsViewModel( + coroutinesTestRule.testDispatcherProvider, + setting, + pixels, + ) + } + + @Test + fun whenViewCreatedAndSettingEnabledThenViewStateUpdated() = runTest { + whenever(setting.isEnabled()).thenReturn(true) + testee.onCreate(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.enabled) + } + } + } + + @Test + fun whenViewCreatedAndSettingDisabledThenViewStateUpdated() = runTest { + whenever(setting.isEnabled()).thenReturn(false) + testee.onCreate(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertFalse(it.enabled) + } + } + } + + @Test + fun whenSettingEnabledThenPixelFired() = runTest { + testee.onSettingEnabled(true) + verify(pixels).fireShortcutSectionToggled(true) + } + + @Test + fun whenSettingDisabledThenPixelFired() = runTest { + testee.onSettingEnabled(false) + verify(pixels).fireShortcutSectionToggled(false) + } +} diff --git a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsViewModelTest.kt b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsViewModelTest.kt index 13140a0e15ff..6240a2873a9b 100644 --- a/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsViewModelTest.kt +++ b/new-tab-page/new-tab-page-impl/src/test/java/com/duckduckgo/newtabpage/impl/shortcuts/ShortcutsViewModelTest.kt @@ -19,64 +19,71 @@ package com.duckduckgo.newtabpage.impl.shortcuts import androidx.lifecycle.LifecycleOwner import app.cash.turbine.test import com.duckduckgo.common.test.CoroutineTestRule -import com.duckduckgo.newtabpage.api.NewTabPageShortcutPlugin -import com.duckduckgo.newtabpage.api.NewTabShortcut -import com.duckduckgo.newtabpage.api.NewTabShortcut.Chat -import junit.framework.TestCase.assertEquals +import com.duckduckgo.newtabpage.impl.FakeShortcut +import com.duckduckgo.newtabpage.impl.FakeShortcutPlugin +import com.duckduckgo.newtabpage.impl.FakeShortcutPluginPoint +import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels +import com.duckduckgo.newtabpage.impl.settings.NewTabSettingsStore import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever class ShortcutsViewModelTest { @get:Rule - var coroutinesTestRule = CoroutineTestRule() - - private var shortcutsProvider: NewTabShortcutsProvider = mock() - private val mockOwner: LifecycleOwner = mock() + var coroutineRule = CoroutineTestRule() private lateinit var testee: ShortcutsViewModel + private var mockLifecycleOwner: LifecycleOwner = mock() + private val newTabSettingsStore: NewTabSettingsStore = mock() + private val newTabShortcutsProvider: NewTabShortcutsProvider = mock() + private val pixels: NewTabPixels = mock() + private val shortcutPlugins = FakeShortcutPluginPoint() + @Before - fun setUp() { - testee = ShortcutsViewModel(coroutinesTestRule.testDispatcherProvider, shortcutsProvider) + fun setup() { + testee = ShortcutsViewModel( + coroutineRule.testDispatcherProvider, + newTabSettingsStore, + newTabShortcutsProvider, + pixels, + ) } @Test - fun whenViewModelStartsThenInitialViewStateProvided() = runTest { + fun whenViewModelStartsAndNoShortcutsThenViewStateShortcutsAreEmpty() = runTest { + whenever(newTabShortcutsProvider.provideActiveShortcuts()).thenReturn(flowOf(emptyList())) + testee.onResume(mockLifecycleOwner) testee.viewState.test { - testee.onStart(mockOwner) expectMostRecentItem().also { - assertEquals(it.shortcuts.size, 0) + assertTrue(it.shortcuts.isEmpty()) } } } @Test - fun whenViewModelStartsThenShortcutsProvider() = runTest { + fun whenViewModelStartsAndSomeShortcutsThenViewStateShortcutsAreNotEmpty() = runTest { + whenever(newTabShortcutsProvider.provideActiveShortcuts()).thenReturn(flowOf(shortcutPlugins.getPlugins())) + testee.onResume(mockLifecycleOwner) testee.viewState.test { - whenever(shortcutsProvider.provideShortcuts()).thenReturn(flowOf(someShortcuts())) - testee.onStart(mockOwner) expectMostRecentItem().also { - assertEquals(it.shortcuts.size, 0) + assertTrue(it.shortcuts.isNotEmpty()) } } } - private fun someShortcuts(): List { - return listOf( - FakeShortcutPlugin(Chat), - FakeShortcutPlugin(NewTabShortcut.Bookmarks), - ) - } + @Test + fun whenShortcutPressedThenPixelFired() { + val shortcut = FakeShortcut("bookmarks") + testee.onShortcutPressed(FakeShortcutPlugin(shortcut)) - class FakeShortcutPlugin(val fakeShortcut: NewTabShortcut) : NewTabPageShortcutPlugin { - override fun getShortcut(): NewTabShortcut { - return fakeShortcut - } + verify(pixels).fireShortcutPressed(shortcut.name) } } diff --git a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessageModel.kt b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessageModel.kt index 02b25c3f491a..3d263847d88e 100644 --- a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessageModel.kt +++ b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessageModel.kt @@ -20,6 +20,8 @@ import kotlinx.coroutines.flow.Flow interface RemoteMessageModel { + fun getActiveMessage(): RemoteMessage? + fun getActiveMessages(): Flow suspend fun onMessageShown(remoteMessage: RemoteMessage) diff --git a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessagingRepository.kt b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessagingRepository.kt index a40bef6fa0c4..b8ad5a24c939 100644 --- a/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessagingRepository.kt +++ b/remote-messaging/remote-messaging-api/src/main/java/com/duckduckgo/remote/messaging/api/RemoteMessagingRepository.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.Flow interface RemoteMessagingRepository { fun activeMessage(message: RemoteMessage?) + fun message(): RemoteMessage? fun messageFlow(): Flow suspend fun dismissMessage(id: String) fun dismissedMessages(): List diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepository.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepository.kt index ca953bf010a5..88a3287b82de 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepository.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/AppRemoteMessagingRepository.kt @@ -53,6 +53,20 @@ class AppRemoteMessagingRepository( remoteMessagesDao.insert(message.copy(shown = true)) } + override fun message(): RemoteMessage? { + val message = remoteMessagesDao.message() + if (message == null || message.message.isEmpty()) return null + + val remoteMessage = messageMapper.fromMessage(message.message) ?: return null + RemoteMessage( + id = message.id, + content = remoteMessage.content, + emptyList(), + emptyList(), + ) + return remoteMessage + } + override fun messageFlow(): Flow { return remoteMessagesDao.messagesFlow().distinctUntilChanged().map { if (it == null || it.message.isEmpty()) return@map null diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/RealRemoteMessageModel.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/RealRemoteMessageModel.kt index ab030b44150a..e478fdfd7574 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/RealRemoteMessageModel.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/RealRemoteMessageModel.kt @@ -34,6 +34,8 @@ class RealRemoteMessageModel @Inject constructor( private val remoteMessagingPixels: RemoteMessagingPixels, private val dispatchers: DispatcherProvider, ) : RemoteMessageModel { + + override fun getActiveMessage(): RemoteMessage? = remoteMessagingRepository.message() override fun getActiveMessages() = remoteMessagingRepository.messageFlow() override suspend fun onMessageShown(remoteMessage: RemoteMessage) { diff --git a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt index 688d4d1bf940..1801c9f09292 100644 --- a/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt +++ b/remote-messaging/remote-messaging-impl/src/main/java/com/duckduckgo/remote/messaging/impl/newtab/RemoteMessageView.kt @@ -45,6 +45,7 @@ import com.duckduckgo.navigation.api.GlobalActivityStarter.DeeplinkActivityParam import com.duckduckgo.newtabpage.api.NewTabPageSection import com.duckduckgo.newtabpage.api.NewTabPageSectionPlugin import com.duckduckgo.remote.messaging.api.RemoteMessage +import com.duckduckgo.remote.messaging.api.RemoteMessageModel import com.duckduckgo.remote.messaging.impl.R import com.duckduckgo.remote.messaging.impl.databinding.ViewRemoteMessageBinding import com.duckduckgo.remote.messaging.impl.mappers.asMessage @@ -106,8 +107,6 @@ class RemoteMessageView @JvmOverloads constructor( viewModel.commands() .onEach { processCommands(it) } .launchIn(coroutineScope!!) - - configureViews() } private fun render(viewState: ViewState) { @@ -130,9 +129,6 @@ class RemoteMessageView @JvmOverloads constructor( } } - private fun configureViews() { - } - private fun showRemoteMessage( message: RemoteMessage, newMessage: Boolean, @@ -184,7 +180,10 @@ class RemoteMessageView @JvmOverloads constructor( } } - private fun launchSharePromoRMFPageChooser(url: String, shareTitle: String) { + private fun launchSharePromoRMFPageChooser( + url: String, + shareTitle: String, + ) { val share = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, url) @@ -213,11 +212,19 @@ class RemoteMessageView @JvmOverloads constructor( @ContributesActivePlugin( AppScope::class, boundType = NewTabPageSectionPlugin::class, + priority = 1, ) -class RemoteMessageNewTabSectionPlugin @Inject constructor() : NewTabPageSectionPlugin { +class RemoteMessageNewTabSectionPlugin @Inject constructor( + private val remoteMessageModel: RemoteMessageModel, +) : NewTabPageSectionPlugin { override val name = NewTabPageSection.REMOTE_MESSAGING_FRAMEWORK.name override fun getView(context: Context): View { return RemoteMessageView(context) } + + override suspend fun isUserEnabled(): Boolean { + val message = remoteMessageModel.getActiveMessage() + return message != null + } } diff --git a/app/src/test/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageViewModelTest.kt b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/newtab/RemoteMessageViewModelTest.kt similarity index 99% rename from app/src/test/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageViewModelTest.kt rename to remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/newtab/RemoteMessageViewModelTest.kt index 4670f93edb22..9da05a21de65 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/remotemessage/RemoteMessageViewModelTest.kt +++ b/remote-messaging/remote-messaging-impl/src/test/java/com/duckduckgo/remote/messaging/newtab/RemoteMessageViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.duckduckgo.app.browser.remotemessage +package com.duckduckgo.remote.messaging.newtab import androidx.lifecycle.LifecycleOwner import app.cash.turbine.test @@ -181,5 +181,3 @@ class RemoteMessageViewModelTest { return remoteMessage } } - -class Test diff --git a/remote-messaging/remote-messaging-store/src/main/java/com/duckduckgo/remote/messaging/store/RemoteMessagesDao.kt b/remote-messaging/remote-messaging-store/src/main/java/com/duckduckgo/remote/messaging/store/RemoteMessagesDao.kt index 02442d5abfbc..a1ca61312043 100644 --- a/remote-messaging/remote-messaging-store/src/main/java/com/duckduckgo/remote/messaging/store/RemoteMessagesDao.kt +++ b/remote-messaging/remote-messaging-store/src/main/java/com/duckduckgo/remote/messaging/store/RemoteMessagesDao.kt @@ -39,6 +39,9 @@ abstract class RemoteMessagesDao { @Query("update remote_message set status = :newState where id = :id") abstract fun updateState(id: String, newState: Status) + @Query("select * from remote_message where status = \"SCHEDULED\"") + abstract fun message(): RemoteMessageEntity? + @Query("select * from remote_message where status = \"SCHEDULED\"") abstract fun messagesFlow(): Flow diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/SavedSitesPixelName.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/SavedSitesPixelName.kt index 85634ba4972d..eaca277f40e2 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/SavedSitesPixelName.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/SavedSitesPixelName.kt @@ -18,23 +18,36 @@ package com.duckduckgo.savedsites.impl import com.duckduckgo.app.statistics.pixels.Pixel enum class SavedSitesPixelName(override val pixelName: String) : Pixel.PixelName { + /** Bookmarks Screen **/ BOOKMARK_IMPORT_SUCCESS("m_bi_s"), BOOKMARK_IMPORT_ERROR("m_bi_e"), BOOKMARK_EXPORT_SUCCESS("m_be_a"), BOOKMARK_EXPORT_ERROR("m_be_e"), - FAVORITE_BOOKMARKS_ITEM_PRESSED("m_fav_b"), - BOOKMARK_LAUNCHED("m_bookmark_launched"), + BOOKMARK_LAUNCHED_DAILY("m_bookmark_launched_daily"), + /** Edit Bookmark Dialog **/ EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED("m_edit_bookmark_add_favorite"), + EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY("m_edit_bookmark_add_favorite_daily"), EDIT_BOOKMARK_REMOVE_FAVORITE_TOGGLED("m_edit_bookmark_remove_favorite"), EDIT_BOOKMARK_DELETE_BOOKMARK_CLICKED("m_edit_bookmark_delete"), EDIT_BOOKMARK_DELETE_BOOKMARK_CONFIRMED("m_edit_bookmark_delete_confirm"), EDIT_BOOKMARK_DELETE_BOOKMARK_CANCELLED("m_edit_bookmark_delete_cancel"), + /** Browser Menu **/ BOOKMARK_MENU_ADD_FAVORITE_CLICKED("m_bookmark_menu_add_favorite"), BOOKMARK_MENU_EDIT_BOOKMARK_CLICKED("m_bookmark_menu_edit"), BOOKMARK_MENU_REMOVE_FAVORITE_CLICKED("m_bookmark_menu_remove_favorite"), BOOKMARK_MENU_DELETE_BOOKMARK_CLICKED("m_bookmark_menu_delete"), + + /** New Tab Pixels **/ + FAVOURITES_LIST_EXPANDED("m_new_tab_page_favorites_expanded"), + FAVOURITES_LIST_COLLAPSED("m_new_tab_page_favorites_collapsed"), + FAVOURITES_TOOLTIP_PRESSED("m_new_tab_page_favorites_info_tooltip"), + FAVOURITES_SECTION_TOGGLED_OFF("m_new_tab_page_customize_section_off_favorites"), + FAVOURITES_SECTION_TOGGLED_ON("m_new_tab_page_customize_section_on_favorites"), + FAVOURITE_CLICKED("m_favorite_clicked"), + FAVOURITE_CLICKED_DAILY("m_favorite_clicked_daily"), + MENU_ACTION_ADD_FAVORITE_PRESSED_DAILY("m_nav_af_p_daily"), } diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModel.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModel.kt index 81928189e992..920b2a1bbadd 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModel.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/bookmarks/BookmarksViewModel.kt @@ -22,6 +22,7 @@ import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.SingleLiveEvent import com.duckduckgo.di.scopes.ActivityScope @@ -133,6 +134,7 @@ class BookmarksViewModel @Inject constructor( override fun onFavoriteAdded() { pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) + pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY, type = DAILY) } override fun onFavoriteRemoved() { @@ -157,6 +159,7 @@ class BookmarksViewModel @Inject constructor( pixel.fire(SavedSitesPixelName.FAVORITE_BOOKMARKS_ITEM_PRESSED) } pixel.fire(SavedSitesPixelName.BOOKMARK_LAUNCHED) + pixel.fire(SavedSitesPixelName.BOOKMARK_LAUNCHED_DAILY, type = DAILY) command.value = OpenSavedSite(savedSite.url) } diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/BookmarksShortcut.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/BookmarksShortcut.kt index 93abc4f2c34c..a0c725d184e4 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/BookmarksShortcut.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/BookmarksShortcut.kt @@ -16,19 +16,63 @@ package com.duckduckgo.savedsites.impl.newtab +import android.content.Context import com.duckduckgo.anvil.annotations.ContributesActivePlugin +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.browser.api.ui.BrowserScreens.BookmarksScreenNoParams import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.navigation.api.GlobalActivityStarter import com.duckduckgo.newtabpage.api.NewTabPageShortcutPlugin import com.duckduckgo.newtabpage.api.NewTabShortcut -import com.duckduckgo.newtabpage.api.NewTabShortcut.Bookmarks +import com.duckduckgo.saved.sites.impl.R import javax.inject.Inject @ContributesActivePlugin( AppScope::class, boundType = NewTabPageShortcutPlugin::class, + priority = 1, ) -class BookmarksNewTabShortcutPlugin @Inject constructor() : NewTabPageShortcutPlugin { +class BookmarksNewTabShortcutPlugin @Inject constructor( + private val globalActivityStarter: GlobalActivityStarter, + private val setting: BookmarksNewTabShortcutSetting, +) : NewTabPageShortcutPlugin { + + inner class BookmarksShortcut() : NewTabShortcut { + override fun name(): String = "bookmarks" + override fun titleResource(): Int = R.string.newTabPageShortcutBookmarks + override fun iconResource(): Int = R.drawable.ic_shortcut_bookmarks + } + override fun getShortcut(): NewTabShortcut { - return Bookmarks + return BookmarksShortcut() + } + + override fun onClick(context: Context) { + globalActivityStarter.start(context, BookmarksScreenNoParams) + } + + override suspend fun isUserEnabled(): Boolean { + return setting.self().isEnabled() } + + override suspend fun setUserEnabled(enabled: Boolean) { + if (enabled) { + setting.self().setEnabled(Toggle.State(true)) + } else { + setting.self().setEnabled(Toggle.State(false)) + } + } +} + +/** + * Local feature/settings - they will never be in remote config + */ +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "bookmarksNewTabShortcutSetting", +) +interface BookmarksNewTabShortcutSetting { + @Toggle.DefaultValue(true) + fun self(): Toggle } diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouriteNewTabSectionItemView.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouriteNewTabSectionItemView.kt new file mode 100644 index 000000000000..b0b80f457dac --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouriteNewTabSectionItemView.kt @@ -0,0 +1,150 @@ +/* + * 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.savedsites.impl.newtab + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.widget.FrameLayout +import android.widget.ImageView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.duckduckgo.common.ui.view.gone +import com.duckduckgo.common.ui.view.hide +import com.duckduckgo.common.ui.view.show +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.saved.sites.impl.R +import com.duckduckgo.saved.sites.impl.databinding.ViewFavouriteSectionItemBinding + +class FavouriteNewTabSectionItemView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : FrameLayout(context, attrs, defStyleAttr) { + + private val binding: ViewFavouriteSectionItemBinding by viewBinding() + + init { + context.obtainStyledAttributes( + attrs, + R.styleable.FavouriteNewTabSectionItemView, + 0, + 0, + ).apply { + setPrimaryText(getString(R.styleable.FavouriteNewTabSectionItemView_primaryText)) + if (hasValue(R.styleable.FavouriteNewTabSectionItemView_leadingIcon)) { + setLeadingIconDrawable(getDrawable(R.styleable.FavouriteNewTabSectionItemView_leadingIcon)!!) + } + + if (hasValue(R.styleable.FavouriteNewTabSectionItemView_gridItemType)) { + val itemType = FavouriteItemType.from(getInt(R.styleable.FavouriteNewTabSectionItemView_gridItemType, 0)) + setItemType(itemType) + } else { + setItemType(FavouriteItemType.Favicon) + } + recycle() + } + } + + /** Sets the primary text title */ + fun setPrimaryText(text: String?) { + binding.quickAccessTitle.text = text + } + + /** Sets the primary text title */ + fun setPrimaryText(@StringRes text: Int) { + binding.quickAccessTitle.text = context.getString(text) + } + + /** Sets the leading icon image drawable */ + fun setLeadingIconDrawable(@DrawableRes drawable: Int) { + binding.quickAccessFavicon.setImageResource(drawable) + } + + /** Sets the leading icon image drawable */ + fun setLeadingIconDrawable(drawable: Drawable) { + binding.quickAccessFavicon.setImageDrawable(drawable) + } + + /** Sets the item click listener */ + fun setClickListener(onClick: () -> Unit) { + binding.quickAccessFaviconCard.setOnClickListener { onClick() } + } + + /** Sets the item click listener */ + fun setLongClickListener(onClick: OnLongClickListener) { + binding.quickAccessFaviconCard.setOnLongClickListener(onClick) + } + + @SuppressLint("ClickableViewAccessibility") + fun setTouchListener(onTouch: OnTouchListener) { + binding.quickAccessFaviconCard.setOnTouchListener(onTouch) + } + + fun favicon(): ImageView { + return binding.quickAccessFavicon + } + + fun hideTitle() { + binding.quickAccessTitle.alpha = 0f + } + + fun showTitle() { + binding.quickAccessTitle.alpha = 1f + } + + /** Sets the item type (see https://www.figma.com/file/6Yfag3rmVECFxs9PTYXdIt/New-Tab-page-exploration-(iOS%2FAndroid)?type=design&node-id=590-31843&mode=design&t=s7gAJlxNYHG02uJl-4 */ + fun setItemType(itemType: FavouriteItemType) { + when (itemType) { + FavouriteItemType.Favicon -> setAsFavicon() + FavouriteItemType.Placeholder -> setAsPlaceholder() + } + } + + private fun setAsPlaceholder() { + binding.quickAccessFaviconCard.setOnClickListener { } + binding.quickAccessTitle.hide() + binding.quickAccessFavicon.gone() + binding.quickAccessFaviconCard.gone() + binding.gridItemPlaceholder.show() + } + + private fun setAsFavicon() { + binding.quickAccessTitle.show() + binding.quickAccessFavicon.show() + binding.quickAccessFaviconCard.show() + binding.gridItemPlaceholder.gone() + } + + enum class FavouriteItemType { + Favicon, + Placeholder, + ; + + companion object { + fun from(type: Int): FavouriteItemType { + // same order as attrs-lists.xml + return when (type) { + 0 -> Favicon + 1 -> Placeholder + else -> Favicon + } + } + } + } +} diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionView.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionView.kt index 7ebbd17ad76a..8f1a6a290068 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionView.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionView.kt @@ -16,17 +16,20 @@ package com.duckduckgo.savedsites.impl.newtab +import android.animation.ValueAnimator import android.annotation.SuppressLint import android.content.Context -import android.text.Html.FROM_HTML_MODE_LEGACY +import android.content.res.Configuration import android.text.Spanned import android.util.AttributeSet import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup.LayoutParams import android.widget.LinearLayout import android.widget.PopupWindow import androidx.core.text.HtmlCompat import androidx.core.text.toSpannable +import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner @@ -37,13 +40,13 @@ import com.duckduckgo.anvil.annotations.ContributesActivePlugin import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.app.tabs.BrowserNav +import com.duckduckgo.common.ui.DuckDuckGoFragment import com.duckduckgo.common.ui.menu.PopupMenu import com.duckduckgo.common.ui.recyclerviewext.GridColumnCalculator import com.duckduckgo.common.ui.recyclerviewext.disableAnimation import com.duckduckgo.common.ui.recyclerviewext.enableAnimation import com.duckduckgo.common.ui.view.gone import com.duckduckgo.common.ui.view.makeSnackbarWithNoBottomInset -import com.duckduckgo.common.ui.view.shape.DaxBubbleEdgeTreatment import com.duckduckgo.common.ui.view.show import com.duckduckgo.common.ui.view.toPx import com.duckduckgo.common.ui.viewbinding.viewBinding @@ -57,11 +60,18 @@ import com.duckduckgo.saved.sites.impl.R import com.duckduckgo.saved.sites.impl.databinding.ViewNewTabFavouritesSectionBinding import com.duckduckgo.saved.sites.impl.databinding.ViewNewTabFavouritesTooltipBinding import com.duckduckgo.savedsites.api.models.SavedSite +import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark +import com.duckduckgo.savedsites.api.models.SavedSite.Favorite +import com.duckduckgo.savedsites.api.models.SavedSitesNames +import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment +import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.DeleteBookmarkListener +import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment.EditSavedSiteListener import com.duckduckgo.savedsites.impl.newtab.FavouriteNewTabSectionsItem.FavouriteItemFavourite import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteFavoriteConfirmation import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteSavedSiteConfirmation import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.ShowEditSavedSiteDialog +import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.SavedSiteChangedViewState import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.ViewState import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionsAdapter.Companion.QUICK_ACCESS_GRID_MAX_COLUMNS import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionsAdapter.Companion.QUICK_ACCESS_ITEM_MAX_SIZE_DP @@ -75,7 +85,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import logcat.logcat @InjectWith(ViewScope::class) class FavouritesNewTabSectionView @JvmOverloads constructor( @@ -95,6 +104,8 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( private var coroutineScope: CoroutineScope? = null + private var isExpandable = true + private val binding: ViewNewTabFavouritesSectionBinding by viewBinding() private lateinit var adapter: FavouritesNewTabSectionsAdapter @@ -104,6 +115,26 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[FavouritesNewTabSectionViewModel::class.java] } + private val expandAnimator: ValueAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = 250L + addUpdateListener { + val progress = it.animatedValue as Float + binding.newTabFavoritesToggle.rotation = progress * 180 + } + } + + init { + context.obtainStyledAttributes( + attrs, + R.styleable.FavouritesNewTabSectionView, + 0, + R.style.Widget_DuckDuckGo_FavouritesNewTabSection, + ).apply { + isExpandable = getBoolean(R.styleable.FavouritesNewTabSectionView_isExpandable, true) + recycle() + } + } + override fun onAttachedToWindow() { AndroidSupportInjection.inject(this) super.onAttachedToWindow() @@ -146,15 +177,10 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( private fun showNewTabFavouritesPopup(anchor: View) { val popupContent = ViewNewTabFavouritesTooltipBinding.inflate(LayoutInflater.from(context)) popupContent.cardView.cardElevation = PopupMenu.POPUP_DEFAULT_ELEVATION_DP.toPx() - val cornerRadius = resources.getDimension(com.duckduckgo.mobile.android.R.dimen.mediumShapeCornerRadius) - val cornerSize = resources.getDimension(com.duckduckgo.mobile.android.R.dimen.daxBubbleDialogEdge) - val distanceFromEdgeInDp = resources.getDimension(com.duckduckgo.mobile.android.R.dimen.daxBubbleDialogDistanceFromEdge) - val distanceFromEdge = popupContent.cardView.width - distanceFromEdgeInDp.toPx() - val edgeTreatment = DaxBubbleEdgeTreatment(cornerSize, distanceFromEdge) + val cornerRadius = resources.getDimension(com.duckduckgo.mobile.android.R.dimen.mediumShapeCornerRadius) popupContent.cardView.shapeAppearanceModel = ShapeAppearanceModel.builder() .setAllCornerSizes(cornerRadius) - .setTopEdge(edgeTreatment) .build() popupContent.cardContent.text = HtmlCompat.fromHtml(context.getString(R.string.newTabPageFavoritesTooltip), HtmlCompat.FROM_HTML_MODE_LEGACY) @@ -165,10 +191,30 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( LayoutParams.WRAP_CONTENT, true, ).apply { + viewModel.onTooltipPressed() showAsDropDown(anchor) } } + // BrowserTabFragment overrides onConfigurationChange, so we have to do this too + override fun onConfigurationChanged(newConfig: Configuration?) { + super.onConfigurationChanged(newConfig) + configureQuickAccessGridLayout(binding.quickAccessRecyclerView) + restorePlaceholders() + } + + private fun restorePlaceholders() { + if (viewModel.viewState.value.favourites.isEmpty()) { + val gridColumnCalculator = GridColumnCalculator(context) + val numOfColumns = gridColumnCalculator.calculateNumberOfColumns(QUICK_ACCESS_ITEM_MAX_SIZE_DP, QUICK_ACCESS_GRID_MAX_COLUMNS) + if (numOfColumns == QUICK_ACCESS_GRID_MAX_COLUMNS) { + adapter.submitList(FavouritesNewTabSectionsAdapter.LANDSCAPE_PLACEHOLDERS) + } else { + adapter.submitList(FavouritesNewTabSectionsAdapter.PORTRAIT_PLACEHOLDERS) + } + } + } + private fun configureQuickAccessGridLayout(recyclerView: RecyclerView) { val gridColumnCalculator = GridColumnCalculator(context) val numOfColumns = gridColumnCalculator.calculateNumberOfColumns(QUICK_ACCESS_ITEM_MAX_SIZE_DP, QUICK_ACCESS_GRID_MAX_COLUMNS) @@ -213,17 +259,17 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( } private fun submitUrl(url: String) { + viewModel.onFavoriteClicked() context.startActivity(browserNav.openInCurrentTab(context, url)) } private fun render(viewState: ViewState) { - logcat { "New Tab: showHome favourites empty ${viewState.favourites.isEmpty()}" } val gridColumnCalculator = GridColumnCalculator(context) val numOfColumns = gridColumnCalculator.calculateNumberOfColumns(QUICK_ACCESS_ITEM_MAX_SIZE_DP, QUICK_ACCESS_GRID_MAX_COLUMNS) if (viewState.favourites.isEmpty()) { - binding.newTabFavoritesToggle.gone() - binding.sectionHeaderOverflowIcon.show() + binding.newTabFavoritesToggleLayout.gone() + binding.sectionHeaderLayout.show() binding.sectionHeaderLayout.setOnClickListener { showNewTabFavouritesPopup(binding.sectionHeaderOverflowIcon) } @@ -234,66 +280,41 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( } } else { binding.sectionHeaderLayout.setOnClickListener(null) - binding.sectionHeaderOverflowIcon.gone() + binding.sectionHeaderLayout.gone() - val numOfCollapsedItems = numOfColumns * 2 - logcat { "New Tab: fav size ${viewState.favourites.size} numOfCollapsedItems $numOfCollapsedItems" } - val showToggle = viewState.favourites.size > numOfCollapsedItems - val showCollapsed = !adapter.expanded + if (isExpandable) { + val numOfCollapsedItems = numOfColumns * 2 + val showToggle = viewState.favourites.size > numOfCollapsedItems + val showCollapsed = !adapter.expanded - if (showCollapsed) { - adapter.submitList(viewState.favourites.take(numOfCollapsedItems).map { FavouriteItemFavourite(it) }) - } else { - adapter.submitList(viewState.favourites.map { FavouriteItemFavourite(it) }) - } - - val favoritesToggle = binding.newTabFavoritesToggle - if (showToggle) { - favoritesToggle.show() if (showCollapsed) { - favoritesToggle.text = context.getString(R.string.newTabFavoritesShowMore) - favoritesToggle.setCompoundDrawablesWithIntrinsicBounds( - 0, - 0, - R.drawable.ic_chevron_small_down_16, - 0, - ) + adapter.submitList(viewState.favourites.take(numOfCollapsedItems).map { FavouriteItemFavourite(it) }) } else { - favoritesToggle.text = context.getString(R.string.newTabFavoritesShowLess) - favoritesToggle.setCompoundDrawablesWithIntrinsicBounds( - 0, - 0, - R.drawable.ic_chevron_small_up_16, - 0, - ) + adapter.submitList(viewState.favourites.map { FavouriteItemFavourite(it) }) } - favoritesToggle.setOnClickListener { - if (adapter.expanded) { - favoritesToggle.text = context.getString(R.string.newTabFavoritesShowMore) - adapter.submitList(viewState.favourites.take(numOfCollapsedItems).map { FavouriteItemFavourite(it) }) - favoritesToggle.setCompoundDrawablesWithIntrinsicBounds( - 0, - 0, - R.drawable.ic_chevron_small_down_16, - 0, - ) - adapter.expanded = false - } else { - favoritesToggle.text = context.getString(R.string.newTabFavoritesShowLess) - adapter.submitList(viewState.favourites.map { FavouriteItemFavourite(it) }) - favoritesToggle.setCompoundDrawablesWithIntrinsicBounds( - 0, - 0, - R.drawable.ic_chevron_small_up_16, - 0, - ) - adapter.expanded = true + + if (showToggle) { + binding.newTabFavoritesToggleLayout.show() + binding.newTabFavoritesToggleLayout.setOnClickListener { + if (adapter.expanded) { + expandAnimator.reverse() + adapter.submitList(viewState.favourites.take(numOfCollapsedItems).map { FavouriteItemFavourite(it) }) + adapter.expanded = false + viewModel.onListCollapsed() + } else { + expandAnimator.start() + adapter.submitList(viewState.favourites.map { FavouriteItemFavourite(it) }) + adapter.expanded = true + viewModel.onListExpanded() + } } + } else { + binding.newTabFavoritesToggleLayout.gone() } } else { - favoritesToggle.gone() + binding.newTabFavoritesToggleLayout.gone() + adapter.submitList(viewState.favourites.map { FavouriteItemFavourite(it) }) } - viewModel.onNewTabFavouritesShown() } } @@ -314,7 +335,7 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( viewModel.onDeleteSavedSiteSnackbarDismissed(it) } - is ShowEditSavedSiteDialog -> TODO() + is ShowEditSavedSiteDialog -> editSavedSite(command.savedSiteChangedViewState) } } @@ -343,16 +364,73 @@ class FavouritesNewTabSectionView @JvmOverloads constructor( ) .show() } + + private fun editSavedSite(savedSiteChangedViewState: SavedSiteChangedViewState) { + val addBookmarkDialog = EditSavedSiteDialogFragment.instance( + savedSiteChangedViewState.savedSite, + savedSiteChangedViewState.bookmarkFolder?.id ?: SavedSitesNames.BOOKMARKS_ROOT, + savedSiteChangedViewState.bookmarkFolder?.name, + ) + val btf = FragmentManager.findFragment(this) + addBookmarkDialog.show(btf.childFragmentManager, ADD_SAVED_SITE_FRAGMENT_TAG) + addBookmarkDialog.listener = object : EditSavedSiteListener { + override fun onFavouriteEdited(favorite: Favorite) { + viewModel.onFavouriteEdited(favorite) + } + + override fun onBookmarkEdited( + bookmark: Bookmark, + oldFolderId: String, + updateFavorite: Boolean, + ) { + viewModel.onBookmarkEdited(bookmark, oldFolderId, updateFavorite) + } + + override fun onFavoriteAdded() { + viewModel.onFavoriteAdded() + } + + override fun onFavoriteRemoved() { + viewModel.onFavoriteRemoved() + } + } + addBookmarkDialog.deleteBookmarkListener = object : DeleteBookmarkListener { + override fun onSavedSiteDeleted(savedSite: SavedSite) { + viewModel.onSavedSiteDeleted(savedSite) + } + + override fun onSavedSiteDeleteCancelled() { + } + + override fun onSavedSiteDeleteRequested() { + } + } + } + + private companion object { + const val EDGE_TREATMENT_DISTANCE_FROM_EDGE = 10f + const val ADD_SAVED_SITE_FRAGMENT_TAG = "ADD_SAVED_SITE" + + // Alignment of popup left edge vs. anchor left edge + const val POPUP_HORIZONTAL_OFFSET_DP = -4 + } } @ContributesActivePlugin( AppScope::class, boundType = NewTabPageSectionPlugin::class, + priority = 3, ) -class FavouritesNewTabSectionPlugin @Inject constructor() : NewTabPageSectionPlugin { +class FavouritesNewTabSectionPlugin @Inject constructor( + private val setting: NewTabFavouritesSectionSetting, +) : NewTabPageSectionPlugin { override val name = NewTabPageSection.FAVOURITES.name override fun getView(context: Context): View { return FavouritesNewTabSectionView(context) } + + override suspend fun isUserEnabled(): Boolean { + return setting.self().isEnabled() + } } diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModel.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModel.kt index 8ef1b5f5c6ec..c409f8e60bdf 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModel.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModel.kt @@ -23,6 +23,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType.DAILY import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.savedsites.api.SavedSitesRepository @@ -30,6 +32,7 @@ import com.duckduckgo.savedsites.api.models.BookmarkFolder import com.duckduckgo.savedsites.api.models.SavedSite import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark import com.duckduckgo.savedsites.api.models.SavedSite.Favorite +import com.duckduckgo.savedsites.impl.SavedSitesPixelName import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteFavoriteConfirmation import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteSavedSiteConfirmation import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.ShowEditSavedSiteDialog @@ -41,6 +44,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -53,6 +57,7 @@ import kotlinx.coroutines.withContext class FavouritesNewTabSectionViewModel @Inject constructor( private val dispatchers: DispatcherProvider, private val savedSitesRepository: SavedSitesRepository, + private val pixel: Pixel, private val faviconManager: FaviconManager, private val syncEngine: SyncEngine, ) : ViewModel(), DefaultLifecycleObserver { @@ -82,11 +87,14 @@ class FavouritesNewTabSectionViewModel @Inject constructor( private val command = Channel(1, BufferOverflow.DROP_OLDEST) internal fun commands(): Flow = command.receiveAsFlow() - override fun onStart(owner: LifecycleOwner) { - super.onStart(owner) + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) viewModelScope.launch(dispatchers.io()) { savedSitesRepository.getFavorites() + .combine(hiddenIds) { favorites, hiddenIds -> + favorites.filter { it.id !in hiddenIds.favorites } + } .flowOn(dispatchers.io()) .onEach { favourites -> withContext(dispatchers.main()) { @@ -104,7 +112,13 @@ class FavouritesNewTabSectionViewModel @Inject constructor( fun onQuickAccessListChanged(newList: List) { viewModelScope.launch(dispatchers.io()) { - savedSitesRepository.updateWithPosition(newList) + val favourites = savedSitesRepository.getFavoritesSync() + if (favourites.size == newList.size) { + savedSitesRepository.updateWithPosition(newList.map { it }) + } else { + val updatedList = newList.plus(favourites.takeLast(favourites.size - newList.size)) + savedSitesRepository.updateWithPosition(updatedList.map { it }) + } } } @@ -206,4 +220,50 @@ class FavouritesNewTabSectionViewModel @Inject constructor( syncEngine.triggerSync(FEATURE_READ) } } + + fun onTooltipPressed() { + pixel.fire(SavedSitesPixelName.FAVOURITES_TOOLTIP_PRESSED) + } + + fun onListExpanded() { + pixel.fire(SavedSitesPixelName.FAVOURITES_LIST_EXPANDED) + } + + fun onListCollapsed() { + pixel.fire(SavedSitesPixelName.FAVOURITES_LIST_COLLAPSED) + } + + fun onFavouriteEdited(favorite: Favorite) { + viewModelScope.launch(dispatchers.io()) { + savedSitesRepository.updateFavourite(favorite) + } + } + + fun onBookmarkEdited( + bookmark: Bookmark, + oldFolderId: String, + updateFavorite: Boolean, + ) { + viewModelScope.launch(dispatchers.io()) { + savedSitesRepository.updateBookmark(bookmark, oldFolderId, updateFavorite) + } + } + + fun onSavedSiteDeleted(savedSite: SavedSite) { + onDeleteSavedSiteRequested(savedSite) + } + + fun onFavoriteAdded() { + pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED) + pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_ADD_FAVORITE_TOGGLED_DAILY, type = DAILY) + } + + fun onFavoriteRemoved() { + pixel.fire(SavedSitesPixelName.EDIT_BOOKMARK_REMOVE_FAVORITE_TOGGLED) + } + + fun onFavoriteClicked() { + pixel.fire(SavedSitesPixelName.FAVOURITE_CLICKED) + pixel.fire(SavedSitesPixelName.FAVOURITE_CLICKED_DAILY, type = DAILY) + } } diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionsAdapter.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionsAdapter.kt index 6e0cef8c299f..c43f4aac9075 100644 --- a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionsAdapter.kt +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionsAdapter.kt @@ -31,19 +31,18 @@ import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ViewHolder import com.duckduckgo.app.browser.favicon.FaviconManager import com.duckduckgo.common.ui.menu.PopupMenu -import com.duckduckgo.common.ui.view.listitem.DaxGridItem.GridItemType.Favicon -import com.duckduckgo.common.ui.view.listitem.DaxGridItem.GridItemType.Placeholder -import com.duckduckgo.mobile.android.databinding.RowNewTabGridItemBinding import com.duckduckgo.saved.sites.impl.R +import com.duckduckgo.saved.sites.impl.databinding.RowFavouriteSectionItemBinding import com.duckduckgo.savedsites.api.models.SavedSite.Favorite -import com.duckduckgo.savedsites.impl.newtab.FavouriteNewTabSectionsItem.FavouriteItemFavourite +import com.duckduckgo.savedsites.impl.newtab.FavouriteNewTabSectionItemView.FavouriteItemType +import com.duckduckgo.savedsites.impl.newtab + .FavouriteNewTabSectionsItem.FavouriteItemFavourite import com.duckduckgo.savedsites.impl.newtab.FavouriteNewTabSectionsItem.PlaceholderItemFavourite import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionsAdapter.FavouriteViewHolder.ItemState.Drag import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionsAdapter.FavouriteViewHolder.ItemState.LongPress import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionsAdapter.FavouriteViewHolder.ItemState.Stale import kotlin.math.absoluteValue import kotlinx.coroutines.launch -import logcat.logcat class FavouritesNewTabSectionsAdapter( private val lifecycleOwner: LifecycleOwner, @@ -93,11 +92,11 @@ class FavouritesNewTabSectionsAdapter( ): ViewHolder { return when (viewType) { PLACEHOLDER_VIEW_TYPE -> PlaceholderViewHolder( - RowNewTabGridItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + RowFavouriteSectionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), ) FAVORITE_TYPE -> FavouriteViewHolder( - RowNewTabGridItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + RowFavouriteSectionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), lifecycleOwner, faviconManager, onMoveListener, @@ -108,7 +107,7 @@ class FavouritesNewTabSectionsAdapter( ) else -> FavouriteViewHolder( - RowNewTabGridItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + RowFavouriteSectionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), lifecycleOwner, faviconManager, onMoveListener, @@ -130,14 +129,14 @@ class FavouritesNewTabSectionsAdapter( } } - private class PlaceholderViewHolder(private val binding: RowNewTabGridItemBinding) : ViewHolder(binding.root) { + private class PlaceholderViewHolder(private val binding: RowFavouriteSectionItemBinding) : ViewHolder(binding.root) { fun bind() { - binding.root.setItemType(Placeholder) + binding.root.setItemType(FavouriteItemType.Placeholder) } } private class FavouriteViewHolder( - private val binding: RowNewTabGridItemBinding, + private val binding: RowFavouriteSectionItemBinding, private val lifecycleOwner: LifecycleOwner, private val faviconManager: FaviconManager, private val onMoveListener: (RecyclerView.ViewHolder) -> Unit, @@ -175,7 +174,7 @@ class FavouritesNewTabSectionsAdapter( item: FavouriteItemFavourite, ) { with(binding.root) { - setItemType(Favicon) + setItemType(FavouriteItemType.Favicon) setPrimaryText(item.favorite.title) loadFavicon(item.favorite.url) configureClickListeners(item.favorite) @@ -203,7 +202,6 @@ class FavouritesNewTabSectionsAdapter( private fun configureClickListeners(favorite: Favorite) { binding.root.setLongClickListener { - logcat { "New Tab: onLongClick" } itemState = LongPress scaleUpFavicon() showOverFlowMenu(binding.root, favorite) diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingView.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingView.kt new file mode 100644 index 000000000000..225c2c94cab4 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingView.kt @@ -0,0 +1,112 @@ +/* + * 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.savedsites.impl.newtab + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeViewModelStoreOwner +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.anvil.annotations.PriorityKey +import com.duckduckgo.common.ui.viewbinding.viewBinding +import com.duckduckgo.common.utils.ViewViewModelFactory +import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.di.scopes.ViewScope +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.newtabpage.api.NewTabPageSection +import com.duckduckgo.newtabpage.api.NewTabPageSectionSettingsPlugin +import com.duckduckgo.saved.sites.impl.databinding.ViewFavouritesSettingsItemBinding +import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSettingsViewModel.ViewState +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.android.support.AndroidSupportInjection +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +@InjectWith(ViewScope::class) +class FavouritesNewTabSettingView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : LinearLayout(context, attrs, defStyle) { + + @Inject + lateinit var viewModelFactory: ViewViewModelFactory + + private val binding: ViewFavouritesSettingsItemBinding by viewBinding() + + private var coroutineScope: CoroutineScope? = null + + private val viewModel: FavouritesNewTabSettingsViewModel by lazy { + ViewModelProvider(findViewTreeViewModelStoreOwner()!!, viewModelFactory)[FavouritesNewTabSettingsViewModel::class.java] + } + + override fun onAttachedToWindow() { + AndroidSupportInjection.inject(this) + super.onAttachedToWindow() + + findViewTreeLifecycleOwner()?.lifecycle?.addObserver(viewModel) + + @SuppressLint("NoHardcodedCoroutineDispatcher") + coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + + viewModel.viewState + .onEach { render(it) } + .launchIn(coroutineScope!!) + } + + private fun render(viewState: ViewState) { + binding.root.quietlySetIsChecked(viewState.enabled) { _, enabled -> + viewModel.onSettingEnabled(enabled) + } + } +} + +@ContributesMultibinding(scope = ActivityScope::class) +@PriorityKey(NewTabPageSectionSettingsPlugin.FAVOURITES) +class FavouritesNewTabSectionSettingsPlugin @Inject constructor() : NewTabPageSectionSettingsPlugin { + override val name = NewTabPageSection.FAVOURITES.name + + override fun getView(context: Context): View { + return FavouritesNewTabSettingView(context) + } + + override suspend fun isActive(): Boolean { + return true + } +} + +/** + * Local feature/settings - they will never be in remote config + */ +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = "newTabFavouritesSectionSetting", +) +interface NewTabFavouritesSectionSetting { + @Toggle.DefaultValue(true) + fun self(): Toggle +} diff --git a/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingsViewModel.kt b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingsViewModel.kt new file mode 100644 index 000000000000..408a73b93ba7 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingsViewModel.kt @@ -0,0 +1,69 @@ +/* + * 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.savedsites.impl.newtab + +import android.annotation.SuppressLint +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.statistics.pixels.Pixel +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.ViewScope +import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.savedsites.impl.SavedSitesPixelName +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@SuppressLint("NoLifecycleObserver") // we don't observe app lifecycle +@ContributesViewModel(ViewScope::class) +class FavouritesNewTabSettingsViewModel @Inject constructor( + private val dispatchers: DispatcherProvider, + private val setting: NewTabFavouritesSectionSetting, + private val pixel: Pixel, +) : ViewModel(), DefaultLifecycleObserver { + + private val _viewState = MutableStateFlow(ViewState(true)) + val viewState = _viewState.asStateFlow() + + data class ViewState(val enabled: Boolean) + + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + + viewModelScope.launch(dispatchers.io()) { + val isEnabled = setting.self().isEnabled() + withContext(dispatchers.main()) { + _viewState.update { ViewState(isEnabled) } + } + } + } + + fun onSettingEnabled(enabled: Boolean) { + setting.self().setEnabled(State(enabled)) + if (enabled) { + pixel.fire(SavedSitesPixelName.FAVOURITES_SECTION_TOGGLED_ON) + } else { + pixel.fire(SavedSitesPixelName.FAVOURITES_SECTION_TOGGLED_OFF) + } + } +} diff --git a/saved-sites/saved-sites-impl/src/main/res/drawable/background_circular_32dp_shape_icon_container.xml b/saved-sites/saved-sites-impl/src/main/res/drawable/background_circular_32dp_shape_icon_container.xml new file mode 100644 index 000000000000..b1255a4120fc --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/drawable/background_circular_32dp_shape_icon_container.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/drawable/favourite_new_tab_favicon_background.xml b/saved-sites/saved-sites-impl/src/main/res/drawable/favourite_new_tab_favicon_background.xml new file mode 100644 index 000000000000..a5dede054911 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/drawable/favourite_new_tab_favicon_background.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/drawable/favourite_new_tab_placeholder_background.xml b/saved-sites/saved-sites-impl/src/main/res/drawable/favourite_new_tab_placeholder_background.xml new file mode 100644 index 000000000000..39e6ccaf94a9 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/drawable/favourite_new_tab_placeholder_background.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/drawable/ic_favorite_24.xml b/saved-sites/saved-sites-impl/src/main/res/drawable/ic_favorite_24.xml new file mode 100644 index 000000000000..e139cade315a --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/drawable/ic_favorite_24.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/saved-sites/saved-sites-impl/src/main/res/drawable/ic_shortcut_bookmarks.xml b/saved-sites/saved-sites-impl/src/main/res/drawable/ic_shortcut_bookmarks.xml new file mode 100644 index 000000000000..c5f347badb2e --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/drawable/ic_shortcut_bookmarks.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/saved-sites/saved-sites-impl/src/main/res/drawable/selectable_circular_32dp_shape_container_ripple.xml b/saved-sites/saved-sites-impl/src/main/res/drawable/selectable_circular_32dp_shape_container_ripple.xml new file mode 100644 index 000000000000..c9df791c93cb --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/drawable/selectable_circular_32dp_shape_container_ripple.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/layout/row_favourite_section_item.xml b/saved-sites/saved-sites-impl/src/main/res/layout/row_favourite_section_item.xml new file mode 100644 index 000000000000..ff0884e55239 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/layout/row_favourite_section_item.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/layout/view_favourite_section_item.xml b/saved-sites/saved-sites-impl/src/main/res/layout/view_favourite_section_item.xml new file mode 100644 index 000000000000..d931baeda5c7 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/layout/view_favourite_section_item.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/layout/view_favourites_settings_item.xml b/saved-sites/saved-sites-impl/src/main/res/layout/view_favourites_settings_item.xml new file mode 100644 index 000000000000..671dec984466 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/layout/view_favourites_settings_item.xml @@ -0,0 +1,27 @@ + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_section.xml b/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_section.xml index 685c8d51295f..bd56ed94faf1 100644 --- a/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_section.xml +++ b/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_section.xml @@ -18,6 +18,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginVertical="@dimen/keyline_4" android:orientation="vertical"> @@ -61,23 +61,28 @@ android:paddingHorizontal="@dimen/keyline_2" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - + android:layout_marginTop="@dimen/keyline_2" + android:paddingHorizontal="@dimen/keyline_5"> + + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_setting_item.xml b/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_setting_item.xml new file mode 100644 index 000000000000..1c50671055cf --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_setting_item.xml @@ -0,0 +1,27 @@ + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_tooltip.xml b/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_tooltip.xml index 3c6c5c24cae0..38b00541cbf8 100644 --- a/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_tooltip.xml +++ b/saved-sites/saved-sites-impl/src/main/res/layout/view_new_tab_favourites_tooltip.xml @@ -1,22 +1,25 @@ + android:layout_height="wrap_content"> + android:layout_marginTop="@dimen/keyline_0" + android:layout_marginEnd="@dimen/keyline_2" + android:layout_marginStart="@dimen/keyline_2" + android:layout_marginBottom="@dimen/keyline_2"> + app:typography="body2" + tools:text="@string/newTabPageFavoritesTooltip" /> diff --git a/saved-sites/saved-sites-impl/src/main/res/values/attrs-saves-sites.xml b/saved-sites/saved-sites-impl/src/main/res/values/attrs-saves-sites.xml new file mode 100644 index 000000000000..3b7f9147a98d --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/values/attrs-saves-sites.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/values/dimen-saved-sites.xml b/saved-sites/saved-sites-impl/src/main/res/values/dimen-saved-sites.xml new file mode 100644 index 000000000000..1e165c1fa7c1 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/values/dimen-saved-sites.xml @@ -0,0 +1,19 @@ + + + + 32dp + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/values/donottranslate.xml b/saved-sites/saved-sites-impl/src/main/res/values/donottranslate.xml index ab3dac5e727f..d7def9ff5d76 100644 --- a/saved-sites/saved-sites-impl/src/main/res/values/donottranslate.xml +++ b/saved-sites/saved-sites-impl/src/main/res/values/donottranslate.xml @@ -19,12 +19,11 @@ Favorites - Add Bookmark and toggle to Add to Favorites.]]> - Show More - Show Less + Add Bookmark and toggle Add to Favorites.]]> Edit Remove From Favorites Delete Favorite removed Undo + Bookmarks \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/main/res/values/styles-saved-sites.xml b/saved-sites/saved-sites-impl/src/main/res/values/styles-saved-sites.xml new file mode 100644 index 000000000000..d3da4a748439 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/main/res/values/styles-saved-sites.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModelTests.kt b/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModelTests.kt index 2b00b15d5c7d..3cba3ed14448 100644 --- a/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModelTests.kt +++ b/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSectionViewModelTests.kt @@ -19,9 +19,11 @@ package com.duckduckgo.savedsites.impl.newtab import androidx.lifecycle.LifecycleOwner import app.cash.turbine.test import com.duckduckgo.app.browser.favicon.FaviconManager +import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.savedsites.api.SavedSitesRepository import com.duckduckgo.savedsites.api.models.SavedSite.Favorite +import com.duckduckgo.savedsites.impl.SavedSitesPixelName import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.DeleteFavoriteConfirmation import com.duckduckgo.savedsites.impl.newtab.FavouritesNewTabSectionViewModel.Command.ShowEditSavedSiteDialog import com.duckduckgo.sync.api.engine.SyncEngine @@ -47,6 +49,7 @@ class FavouritesNewTabSectionViewModelTests { private val mockSavedSitesRepository: SavedSitesRepository = mock() private val faviconManager: FaviconManager = mock() private val syncEngine: SyncEngine = mock() + private val pixel: Pixel = mock() private lateinit var testee: FavouritesNewTabSectionViewModel @@ -56,12 +59,18 @@ class FavouritesNewTabSectionViewModelTests { @Before fun setup() { whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(emptyList())) - testee = FavouritesNewTabSectionViewModel(coroutinesTestRule.testDispatcherProvider, mockSavedSitesRepository, faviconManager, syncEngine) + testee = FavouritesNewTabSectionViewModel( + coroutinesTestRule.testDispatcherProvider, + mockSavedSitesRepository, + pixel, + faviconManager, + syncEngine, + ) } @Test fun whenViewModelIsInitializedThenViewStateShouldEmitInitialState() = runTest { - testee.onStart(mockLifecycleOwner) + testee.onResume(mockLifecycleOwner) testee.viewState.test { expectMostRecentItem().also { @@ -74,7 +83,7 @@ class FavouritesNewTabSectionViewModelTests { fun whenViewModelIsInitializedAndFavouritesPresentThenViewStateShouldEmitCorrectState() = runTest { whenever(mockSavedSitesRepository.getFavorites()).thenReturn(flowOf(listOf(favorite1))) - testee.onStart(mockLifecycleOwner) + testee.onResume(mockLifecycleOwner) testee.viewState.test { expectMostRecentItem().also { @@ -86,7 +95,9 @@ class FavouritesNewTabSectionViewModelTests { @Test fun whenItemsChangedThenRepositoryUpdated() { - val itemsChanged = listOf(favorite1, favorite2) + whenever(mockSavedSitesRepository.getFavoritesSync()).thenReturn(listOf(favorite1, favorite2)) + val itemsChanged = listOf(favorite2, favorite1) + testee.onQuickAccessListChanged(itemsChanged) verify(mockSavedSitesRepository).updateWithPosition(itemsChanged) @@ -126,4 +137,25 @@ class FavouritesNewTabSectionViewModelTests { verify(syncEngine).triggerSync(FEATURE_READ) } + + @Test + fun whenTooltipPressedThenPixelSent() = runTest { + testee.onTooltipPressed() + + verify(pixel).fire(SavedSitesPixelName.FAVOURITES_TOOLTIP_PRESSED) + } + + @Test + fun whenListExpandedThenPixelSent() = runTest { + testee.onListExpanded() + + verify(pixel).fire(SavedSitesPixelName.FAVOURITES_LIST_EXPANDED) + } + + @Test + fun whenListCollapsedThenPixelSent() = runTest { + testee.onListCollapsed() + + verify(pixel).fire(SavedSitesPixelName.FAVOURITES_LIST_COLLAPSED) + } } diff --git a/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingsViewModelTest.kt b/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingsViewModelTest.kt new file mode 100644 index 000000000000..188ad0454391 --- /dev/null +++ b/saved-sites/saved-sites-impl/src/test/java/com/duckduckgo/savedsites/impl/newtab/FavouritesNewTabSettingsViewModelTest.kt @@ -0,0 +1,125 @@ +package com.duckduckgo.savedsites.impl.newtab + +import androidx.lifecycle.LifecycleOwner +import app.cash.turbine.test +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.savedsites.impl.SavedSitesPixelName +import kotlinx.coroutines.test.runTest +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mockito.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class FavouritesNewTabSettingsViewModelTest { + + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private lateinit var testee: FavouritesNewTabSettingsViewModel + private val setting: NewTabFavouritesSectionSetting = mock() + private val lifecycleOwner: LifecycleOwner = mock() + private val pixels: Pixel = mock() + + @Before + fun setup() { + testee = FavouritesNewTabSettingsViewModel( + coroutinesTestRule.testDispatcherProvider, + setting, + pixels, + ) + } + + @Test + fun whenViewCreatedAndSettingEnabledThenViewStateUpdated() = runTest { + whenever(setting.self()).thenReturn( + object : Toggle { + override fun isEnabled(): Boolean { + return true + } + + override fun setEnabled(state: State) { + } + + override fun getRawStoredState(): State { + return State() + } + }, + ) + testee.onCreate(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertTrue(it.enabled) + } + } + } + + @Test + fun whenViewCreatedAndSettingDisabledThenViewStateUpdated() = runTest { + whenever(setting.self()).thenReturn( + object : Toggle { + override fun isEnabled(): Boolean { + return false + } + + override fun setEnabled(state: State) { + } + + override fun getRawStoredState(): State { + return State() + } + }, + ) + testee.onCreate(lifecycleOwner) + testee.viewState.test { + expectMostRecentItem().also { + assertFalse(it.enabled) + } + } + } + + @Test + fun whenSettingEnabledThenPixelFired() = runTest { + whenever(setting.self()).thenReturn( + object : Toggle { + override fun isEnabled(): Boolean { + return false + } + + override fun setEnabled(state: State) { + } + + override fun getRawStoredState(): State { + return State() + } + }, + ) + testee.onSettingEnabled(true) + verify(pixels).fire(SavedSitesPixelName.FAVOURITES_SECTION_TOGGLED_ON) + } + + @Test + fun whenSettingDisabledThenPixelFired() = runTest { + whenever(setting.self()).thenReturn( + object : Toggle { + override fun isEnabled(): Boolean { + return false + } + + override fun setEnabled(state: State) { + } + + override fun getRawStoredState(): State { + return State() + } + }, + ) + testee.onSettingEnabled(false) + verify(pixels).fire(SavedSitesPixelName.FAVOURITES_SECTION_TOGGLED_OFF) + } +} From c19708c3b44b7e1ca00d3f9e0a19ea313d35abfe Mon Sep 17 00:00:00 2001 From: Josh Leibstein Date: Thu, 18 Jul 2024 14:32:34 +0100 Subject: [PATCH 15/20] Handle print exception onWrite (#4775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/488551667048375/1207831722240169/f ### Description Handles the case where a print job has not been cleaned up before starting a new one. ### Steps to test this PR - [x] Search for “dog" - [x] Tap “More results” a few times so that the page is quite long - [x] Tap “Print Page” and quickly tap back before the preview is shown - [x] Quickly tap “Print Page” again - [x] Verify that the retry screen is shown - [x] Tap “Retry” (You may need to wait a little while if the document is large) - [x] Verify that the preview is shown ### UI changes | API 27 | API 34 | Video | | ------ | ----- | ----- | ![retry_api27](https://github.com/user-attachments/assets/d634c7c0-877c-4037-b4ed-0ca2790c9519)|![retry](https://github.com/user-attachments/assets/ac88d923-ca9b-4b8c-ad20-d64240462a30)|![video](https://github.com/user-attachments/assets/91c5d0f5-4c9a-4ec3-a82b-c5120103001e) --- .../app/browser/print/PrintDocumentAdapterFactory.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/print/PrintDocumentAdapterFactory.kt b/app/src/main/java/com/duckduckgo/app/browser/print/PrintDocumentAdapterFactory.kt index 980439465702..dfc2f02b1267 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/print/PrintDocumentAdapterFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/print/PrintDocumentAdapterFactory.kt @@ -22,6 +22,7 @@ import android.os.ParcelFileDescriptor import android.print.PageRange import android.print.PrintAttributes import android.print.PrintDocumentAdapter +import timber.log.Timber class PrintDocumentAdapterFactory { companion object { @@ -52,7 +53,12 @@ class PrintDocumentAdapterFactory { cancellationSignal: CancellationSignal?, callback: WriteResultCallback?, ) { - printDocumentAdapter.onWrite(pages, destination, cancellationSignal, callback) + runCatching { + printDocumentAdapter.onWrite(pages, destination, cancellationSignal, callback) + }.onFailure { exception -> + Timber.e(exception, "Failed to write document") + callback?.onWriteCancelled() + } } override fun onFinish() { From ed471483ed0d49f2bcf998778594726cb616a87a Mon Sep 17 00:00:00 2001 From: Marcos Date: Thu, 18 Jul 2024 16:19:03 +0100 Subject: [PATCH 16/20] Disable omnibar scrolling when on new tab page (#4776) Task/Issue URL: https://app.asana.com/0/72649045549333/1207845944313948/f ### Description Disable omnibar scrolling on new tab page. ### Steps to test this PR See task --- .../main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 64e2e16bfaa0..04b9e7424f77 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1154,6 +1154,7 @@ class BrowserTabFragment : newBrowserTab.newTabContainerLayout.show() binding.browserLayout.gone() webViewContainer.gone() + omnibarScrolling.disableOmnibarScrolling(omnibar.toolbarContainer) omnibar.appBarLayout.setExpanded(true) webView?.onPause() webView?.hide() @@ -3954,6 +3955,7 @@ class BrowserTabFragment : .launchIn(lifecycleScope) newBrowserTab.newTabContainerLayout.show() newBrowserTab.newTabLayout.show() + omnibarScrolling.disableOmnibarScrolling(omnibar.toolbarContainer) viewModel.onNewTabShown() } From 2572ea7f4e6b45d207fd4b3cea8c44da614afe77 Mon Sep 17 00:00:00 2001 From: Josh Leibstein Date: Fri, 19 Jul 2024 09:19:43 +0100 Subject: [PATCH 17/20] Handle quote input (#4762) Task/Issue URL: https://app.asana.com/0/414730916066338/1207180035648641/f ### Description Addresses issue raised here: https://github.com/duckduckgo/Android/issues/4472 ### Steps to test this PR - [x] Enter a URL in quotes (Or containing a quote) - [x] Verify that the input is treated as a search term and not a URL ### UI changes | Before | After | | ------ | ----- | ![ddg](https://github.com/user-attachments/assets/4ce49027-1c42-4747-af95-15f27d0a97e0)|![ddg_after](https://github.com/user-attachments/assets/70cbda19-7812-46bf-aeb5-a33fc0b9d7eb) --- .../duckduckgo/app/global/UriStringTest.kt | 42 ++++++++++++++++++- .../com/duckduckgo/app/browser/UriString.kt | 3 ++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/com/duckduckgo/app/global/UriStringTest.kt b/app/src/test/java/com/duckduckgo/app/global/UriStringTest.kt index 7f03fb5f0f4e..6f372c226870 100644 --- a/app/src/test/java/com/duckduckgo/app/global/UriStringTest.kt +++ b/app/src/test/java/com/duckduckgo/app/global/UriStringTest.kt @@ -391,7 +391,47 @@ class UriStringTest { } @Test - fun whenSchemeIsValidFtpButNotHttpThenNot() { + fun whenSchemeIsValidFtpButNotHttpThenIsFalse() { assertFalse(isWebUrl("ftp://example.com")) } + + @Test + fun whenUrlStartsWithDoubleQuoteThenIsFalse() { + assertFalse(isWebUrl("\"example.com")) + } + + @Test + fun whenUrlStartsWithSingleQuoteThenIsFalse() { + assertFalse(isWebUrl("'example.com")) + } + + @Test + fun whenUrlEndsWithDoubleQuoteThenIsFalse() { + assertFalse(isWebUrl("example.com\"")) + } + + @Test + fun whenUrlEndsWithSingleQuoteThenIsFalse() { + assertFalse(isWebUrl("example.com'")) + } + + @Test + fun whenUrlStartsAndEndsWithDoubleQuoteThenIsFalse() { + assertFalse(isWebUrl("\"example.com\"")) + } + + @Test + fun whenUrlStartsAndEndsWithSingleQuoteThenIsFalse() { + assertFalse(isWebUrl("'example.com'")) + } + + @Test + fun whenUrlContainsDoubleQuoteThenIsFalse() { + assertFalse(isWebUrl("example\".com")) + } + + @Test + fun whenUrlContainsSingleQuoteThenIsFalse() { + assertFalse(isWebUrl("example'.com")) + } } diff --git a/browser-api/src/main/java/com/duckduckgo/app/browser/UriString.kt b/browser-api/src/main/java/com/duckduckgo/app/browser/UriString.kt index a7a07622db59..5ea796d18034 100644 --- a/browser-api/src/main/java/com/duckduckgo/app/browser/UriString.kt +++ b/browser-api/src/main/java/com/duckduckgo/app/browser/UriString.kt @@ -96,6 +96,9 @@ class UriString { } fun isWebUrl(inputQuery: String): Boolean { + if (inputQuery.contains("\"") || inputQuery.contains("'")) { + return false + } if (inputQuery.contains(space)) return false val rawUri = Uri.parse(inputQuery) From f777c5c093be54da3df3933f9b1a09bb2aa2230c Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Fri, 19 Jul 2024 17:03:15 +0100 Subject: [PATCH 18/20] Fix favorites displayed underneath Dax dialog (#4778) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/488551667048375/1207845944313951/f ### Description Fix for not showing favorites displayed underneath the 'visit site' Dax dialog ### Steps to test this PR On each step, wait for searches and sites to fully load in the browser - [x] Fresh install - [x] Perform a search - [x] Save bookmark → Add favorite - [x] Visit a site → ⚠️ Don't dismiss the trackers onboarding CTA - [x] Save bookmark → Add favorite - [x] Open a new tab - [x] Perform a search - [x] Open a new tab - [x] Check favorites are not displayed underneath Dax dialog - [x] Tap on a site suggestion - [x] Check favorites are not displayed underneath Dax dialog before site is loaded ### UI changes | Before | After | | ------ | ----- | ![Screenshot_20240718_140440](https://github.com/user-attachments/assets/e812d811-9f02-4a87-8ae6-80948f2ff2ab)|![Screenshot 2024-07-19 at 12 37 48](https://github.com/user-attachments/assets/4f683010-0599-4380-b0ce-e420a7097d53)| --- .../com/duckduckgo/app/browser/BrowserTabViewModelTest.kt | 4 ++++ .../java/com/duckduckgo/app/browser/BrowserTabViewModel.kt | 3 +-- .../main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index c621ba795ea3..0118907405e2 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -105,6 +105,8 @@ import com.duckduckgo.app.browser.viewstate.LoadingViewState import com.duckduckgo.app.browser.webview.SslWarningLayout.Action import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId +import com.duckduckgo.app.cta.model.CtaId.DAX_DIALOG_NETWORK +import com.duckduckgo.app.cta.model.CtaId.DAX_DIALOG_TRACKERS_FOUND import com.duckduckgo.app.cta.model.CtaId.DAX_END import com.duckduckgo.app.cta.model.DismissedCta import com.duckduckgo.app.cta.ui.Cta @@ -2366,6 +2368,7 @@ class BrowserTabViewModelTest { whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(false) whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(true) whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true) + whenever(mockDismissedCtaDao.exists(DAX_DIALOG_TRACKERS_FOUND)).thenReturn(true) testee.refreshCta() assertNull(testee.ctaViewState.value!!.cta) assertTrue(testee.ctaViewState.value!!.daxOnboardingComplete) @@ -2378,6 +2381,7 @@ class BrowserTabViewModelTest { whenever(mockWidgetCapabilities.supportsAutomaticWidgetAdd).thenReturn(false) whenever(mockWidgetCapabilities.hasInstalledWidgets).thenReturn(true) whenever(mockDismissedCtaDao.exists(DAX_END)).thenReturn(true) + whenever(mockDismissedCtaDao.exists(DAX_DIALOG_NETWORK)).thenReturn(true) testee.refreshCta() assertNull(testee.ctaViewState.value!!.cta) assertTrue(testee.ctaViewState.value!!.daxOnboardingComplete) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index bc11c910334a..c9f1bf1e7009 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -113,7 +113,6 @@ import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.fire.fireproofwebsite.ui.AutomaticFireproofSetting.ALWAYS import com.duckduckgo.app.fire.fireproofwebsite.ui.AutomaticFireproofSetting.ASK_EVERY_TIME -import com.duckduckgo.app.global.* import com.duckduckgo.app.global.events.db.UserEventKey import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.model.PrivacyShield @@ -2442,7 +2441,7 @@ class BrowserTabViewModel @Inject constructor( ) } val isOnboardingComplete = withContext(dispatchers.io()) { - ctaViewModel.daxDialogEndShown() + ctaViewModel.areBubbleDaxDialogsCompleted() } if (isBrowserShowing && cta != null) hasCtaBeenShownForCurrentPage.set(true) ctaViewState.value = currentCtaViewState().copy( diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 987b0ba9d66c..30e8eeb7dc2f 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -288,7 +288,10 @@ class CtaViewModel @Inject constructor( // We only want to show New Tab when the Home CTAs from Onboarding has finished // https://app.asana.com/0/1157893581871903/1207769731595075/f - fun daxDialogEndShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_END) + fun areBubbleDaxDialogsCompleted(): Boolean { + val bubbleCtasShown = daxDialogEndShown() && (daxDialogNetworkShown() || daxDialogOtherShown() || daxDialogTrackersFoundShown()) + return bubbleCtasShown || hideTips() + } private fun daxDialogSerpShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_DIALOG_SERP) @@ -300,6 +303,8 @@ class CtaViewModel @Inject constructor( private fun daxDialogFireEducationShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_FIRE_BUTTON) + private fun daxDialogEndShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_END) + private fun pulseFireButtonShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_FIRE_BUTTON_PULSE) private fun isSerpUrl(url: String): Boolean = url.contains(OnboardingDaxDialogCta.SERP) From 6160f09f87612dcd9d8775ce5bc6ed50ec0c2ba8 Mon Sep 17 00:00:00 2001 From: Craig Russell Date: Mon, 22 Jul 2024 14:32:21 +0100 Subject: [PATCH 19/20] Enforce legacy UX for new tab and focussed view (#4785) Task/Issue URL: https://app.asana.com/0/608920331025315/1207866714282844/f ### Description Enforces legacy UX for new tab and focussed views. We will probably have to revisit this in future to give the _new_ UX for internal users because this PR will enforce legacy experience regardless. ### Steps to test this PR - [ ] Clean install - [ ] Complete onboarding - [ ] Add a favorite - [ ] Open a new tab; verify you see the _old_ UX for new tab screen --- .../com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt | 3 +++ .../com/duckduckgo/app/browser/newtab/NewTabPageProvider.kt | 1 + .../src/main/java/com/duckduckgo/newtabpage/impl/NewTabPage.kt | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt index e00f22387477..0ed14717b3b6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/FocusedViewProvider.kt @@ -49,6 +49,7 @@ class RealFocusedViewProvider @Inject constructor( @ContributesActivePlugin( scope = ActivityScope::class, boundType = FocusedViewPlugin::class, + priority = 0, ) class FocusedLegacyPage @Inject constructor() : FocusedViewPlugin { @@ -62,6 +63,8 @@ class FocusedLegacyPage @Inject constructor() : FocusedViewPlugin { @ContributesActivePlugin( scope = ActivityScope::class, boundType = FocusedViewPlugin::class, + priority = 100, + defaultActiveValue = false, ) class FocusedPage @Inject constructor() : FocusedViewPlugin { diff --git a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabPageProvider.kt b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabPageProvider.kt index 5379e5794c2d..fd54f7636354 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabPageProvider.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/newtab/NewTabPageProvider.kt @@ -48,6 +48,7 @@ class RealNewTabPageProvider @Inject constructor( @ContributesActivePlugin( scope = AppScope::class, boundType = NewTabPagePlugin::class, + priority = 0, ) class NewTabLegacyPage @Inject constructor() : NewTabPagePlugin { diff --git a/new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/NewTabPage.kt b/new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/NewTabPage.kt index be51307f4b01..f69049cd2314 100644 --- a/new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/NewTabPage.kt +++ b/new-tab-page/new-tab-page-impl/src/main/java/com/duckduckgo/newtabpage/impl/NewTabPage.kt @@ -28,7 +28,8 @@ import javax.inject.Inject @ContributesActivePlugin( scope = AppScope::class, boundType = NewTabPagePlugin::class, - priority = 100, // higher to come last in the list of plugins + priority = 100, // higher to come last in the list of plugins, + defaultActiveValue = false, ) class NewTabPage @Inject constructor() : NewTabPagePlugin { From 0c3b5c3d702398c7800ea4a53ef17dda4d30590b Mon Sep 17 00:00:00 2001 From: Dax the Deployer Date: Mon, 22 Jul 2024 10:10:57 -0400 Subject: [PATCH 20/20] Updated release notes and version number for new release - 5.209.0 --- app/version/release-notes | 6 +----- app/version/version.properties | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/version/release-notes b/app/version/release-notes index 5bfdc96c4f62..a43a0bfbca62 100644 --- a/app/version/release-notes +++ b/app/version/release-notes @@ -1,5 +1 @@ -Added the ability to import passwords from the desktop version of the browser via Sync & Backup. - -For Privacy Pro subscribers: -Fixed an issue that prevented some users from connecting to a U.S. server location. -Privacy Pro is currently available to U.S. residents only. \ No newline at end of file +Bug fixes and other improvements \ No newline at end of file diff --git a/app/version/version.properties b/app/version/version.properties index 2686a6649bc1..6b662a683b3e 100644 --- a/app/version/version.properties +++ b/app/version/version.properties @@ -1 +1 @@ -VERSION=5.208.1 \ No newline at end of file +VERSION=5.209.0 \ No newline at end of file