From 353ecf3e724dfbec082ff0edea60a43baf4d4e09 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 9 Apr 2020 12:08:08 +0100 Subject: [PATCH 01/15] Delete privacy store add whitelist functionality and update toggle --- .../19.json | 688 ++++++++++++++++++ .../app/browser/BrowserTabViewModelTest.kt | 11 +- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 12 +- .../app/httpsupgrade/HttpsUpgraderTest.kt | 11 +- .../ui/PrivacyDashboardViewModelTest.kt | 48 +- .../app/privacy/ui/ScorecardViewModelTest.kt | 19 +- .../TrackerDetectorClientTypeTest.kt | 8 +- .../trackerdetection/TrackerDetectorTest.kt | 22 +- .../app/browser/BrowserTabFragment.kt | 7 +- .../app/browser/BrowserTabViewModel.kt | 14 + .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 16 +- .../java/com/duckduckgo/app/di/DaoModule.kt | 3 + .../java/com/duckduckgo/app/di/StoreModule.kt | 3 - .../duckduckgo/app/global/ViewModelFactory.kt | 13 +- .../duckduckgo/app/global/db/AppDatabase.kt | 19 +- .../com/duckduckgo/app/global/model/Site.kt | 4 +- .../app/httpsupgrade/HttpsUpgrader.kt | 14 +- .../httpsupgrade/di/HttpsUpgraderModule.kt | 4 +- .../page/TrackerBlockingSelectionViewModel.kt | 33 - .../app/privacy/db/UserWhitelistDao.kt | 42 ++ .../UserWhitelistedDomain.kt} | 15 +- .../store/PrivacySettingsSharedPreferences.kt | 42 -- .../privacy/ui/PrivacyDashboardActivity.kt | 2 +- .../privacy/ui/PrivacyDashboardViewModel.kt | 66 +- .../privacy/ui/PrivacyPracticesViewModel.kt | 3 +- .../app/privacy/ui/ScorecardViewModel.kt | 46 +- .../privacy/ui/TrackerNetworksViewModel.kt | 3 +- .../duckduckgo/app/trackerdetection/Client.kt | 1 + .../app/trackerdetection/TrackerDataLoader.kt | 6 +- .../app/trackerdetection/TrackerDetector.kt | 14 +- .../di/TrackerDetectionModule.kt | 6 +- 31 files changed, 978 insertions(+), 217 deletions(-) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json delete mode 100644 app/src/main/java/com/duckduckgo/app/onboarding/ui/page/TrackerBlockingSelectionViewModel.kt create mode 100644 app/src/main/java/com/duckduckgo/app/privacy/db/UserWhitelistDao.kt rename app/src/main/java/com/duckduckgo/app/privacy/{store/PrivacySettingsStore.kt => model/UserWhitelistedDomain.kt} (64%) delete mode 100644 app/src/main/java/com/duckduckgo/app/privacy/store/PrivacySettingsSharedPreferences.kt diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json new file mode 100644 index 000000000000..6cbf1e617509 --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/19.json @@ -0,0 +1,688 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "d8d825ec16c73991f251a7f848e71b52", + "entities": [ + { + "tableName": "tds_tracker", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `defaultAction` TEXT NOT NULL, `ownerName` TEXT NOT NULL, `categories` TEXT NOT NULL, `rules` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultAction", + "columnName": "defaultAction", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerName", + "columnName": "ownerName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rules", + "columnName": "rules", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `displayName` TEXT NOT NULL, `prevalence` REAL NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "prevalence", + "columnName": "prevalence", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tds_domain_entity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, `entityName` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entityName", + "columnName": "entityName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "temporary_tracking_whitelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "user_whitelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_bloom_filter_spec", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `errorRate` REAL NOT NULL, `totalEntries` INTEGER NOT NULL, `sha256` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "errorRate", + "columnName": "errorRate", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "totalEntries", + "columnName": "totalEntries", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "https_whitelisted_domain", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))", + "fields": [ + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "domain" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "network_leaderboard", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`networkName` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`networkName`))", + "fields": [ + { + "fieldPath": "networkName", + "columnName": "networkName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "networkName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "sites_visited", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tabId` TEXT NOT NULL, `url` TEXT, `title` TEXT, `skipHome` INTEGER NOT NULL, `viewed` INTEGER NOT NULL, `position` INTEGER NOT NULL, `tabPreviewFile` TEXT, PRIMARY KEY(`tabId`))", + "fields": [ + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "skipHome", + "columnName": "skipHome", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewed", + "columnName": "viewed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabPreviewFile", + "columnName": "tabPreviewFile", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "tabId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tabs_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX `index_tabs_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tab_selection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `tabId` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`tabId`) REFERENCES `tabs`(`tabId`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tabId", + "columnName": "tabId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tab_selection_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX `index_tab_selection_tabId` ON `${TABLE_NAME}` (`tabId`)" + } + ], + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "tabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] + }, + { + "tableName": "bookmarks", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `url` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "survey", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`surveyId` TEXT NOT NULL, `url` TEXT, `daysInstalled` INTEGER, `status` TEXT NOT NULL, PRIMARY KEY(`surveyId`))", + "fields": [ + { + "fieldPath": "surveyId", + "columnName": "surveyId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "daysInstalled", + "columnName": "daysInstalled", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "surveyId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "dismissed_cta", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ctaId` TEXT NOT NULL, PRIMARY KEY(`ctaId`))", + "fields": [ + { + "fieldPath": "ctaId", + "columnName": "ctaId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "ctaId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "search_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_days_used", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` TEXT NOT NULL, PRIMARY KEY(`date`))", + "fields": [ + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "date" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "app_enjoyment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eventType` INTEGER NOT NULL, `promptCount` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `primaryKey` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "eventType", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "promptCount", + "columnName": "promptCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primaryKey", + "columnName": "primaryKey", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "primaryKey" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` TEXT NOT NULL, PRIMARY KEY(`notificationId`))", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "notificationId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "privacy_protection_count", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `blocked_tracker_count` INTEGER NOT NULL, `upgrade_count` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "blockedTrackerCount", + "columnName": "blocked_tracker_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "upgradeCount", + "columnName": "upgrade_count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "UncaughtExceptionEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `exceptionSource` TEXT NOT NULL, `message` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exceptionSource", + "columnName": "exceptionSource", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tdsMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `eTag` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "userStage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` INTEGER NOT NULL, `appStage` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "appStage", + "columnName": "appStage", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd8d825ec16c73991f251a7f848e71b52')" + ] + } +} \ No newline at end of file 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 5b43df010b87..aa9f97295354 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -57,9 +57,9 @@ import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.TestEntity -import com.duckduckgo.app.privacy.store.PrivacySettingsStore import com.duckduckgo.app.runBlocking import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.VariantManager @@ -172,10 +172,10 @@ class BrowserTabViewModelTest { private lateinit var mockWidgetCapabilities: WidgetCapabilities @Mock - private lateinit var mockPrivacySettingsStore: PrivacySettingsStore + private lateinit var mockUserStageStore: UserStageStore @Mock - private lateinit var mockUserStageStore: UserStageStore + private lateinit var mockUserWhitelistDao: UserWhitelistDao private lateinit var mockAutoCompleteApi: AutoCompleteApi @@ -206,10 +206,10 @@ class BrowserTabViewModelTest { mockSurveyDao, mockWidgetCapabilities, mockDismissedCtaDao, + mockUserWhitelistDao, mockVariantManager, mockSettingsStore, mockOnboardingStore, - mockPrivacySettingsStore, mockUserStageStore, coroutineRule.testDispatcherProvider ) @@ -222,7 +222,7 @@ class BrowserTabViewModelTest { whenever(mockTabsRepository.retrieveSiteData(any())).thenReturn(MutableLiveData()) whenever(mockPrivacyPractices.privacyPracticesFor(any())).thenReturn(PrivacyPractices.UNKNOWN) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) - whenever(mockPrivacySettingsStore.privacyOn).thenReturn(true) + whenever(mockUserWhitelistDao.contains(anyString())).thenReturn(true) testee = BrowserTabViewModel( statisticsUpdater = mockStatisticsUpdater, @@ -230,6 +230,7 @@ class BrowserTabViewModelTest { duckDuckGoUrlDetector = DuckDuckGoUrlDetector(), siteFactory = siteFactory, tabRepository = mockTabsRepository, + userWhitelistDao = mockUserWhitelistDao, networkLeaderboardDao = mockNetworkLeaderboardDao, autoComplete = mockAutoCompleteApi, appSettingsPreferencesStore = mockSettingsStore, diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 2a677dfc73b0..9f2534b5b0e6 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -31,11 +31,11 @@ import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.HttpsStatus import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.TestEntity -import com.duckduckgo.app.privacy.store.PrivacySettingsStore import com.duckduckgo.app.runBlocking import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.VariantManager @@ -101,7 +101,7 @@ class CtaViewModelTest { private lateinit var mockOnboardingStore: OnboardingStore @Mock - private lateinit var mockPrivacySettingsStore: PrivacySettingsStore + private lateinit var mockUserWhitelistDao: UserWhitelistDao @Mock private lateinit var mockUserStageStore: UserStageStore @@ -126,7 +126,7 @@ class CtaViewModelTest { .build() whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) - whenever(mockPrivacySettingsStore.privacyOn).thenReturn(true) + whenever(mockUserWhitelistDao.contains(any())).thenReturn(false) testee = CtaViewModel( mockAppInstallStore, @@ -134,10 +134,10 @@ class CtaViewModelTest { mockSurveyDao, mockWidgetCapabilities, mockDismissedCtaDao, + mockUserWhitelistDao, mockVariantManager, mockSettingsDataStore, mockOnboardingStore, - mockPrivacySettingsStore, mockUserStageStore, coroutineRule.testDispatcherProvider ) @@ -310,9 +310,9 @@ class CtaViewModelTest { } @Test - fun whenRefreshCtaWhileBrowsingAndPrivacyOffThenReturnNull() = coroutineRule.runBlocking { + fun whenRefreshCtaWhileBrowsingAndPrivacyOffForSiteThenReturnNull() = coroutineRule.runBlocking { givenDaxOnboardingActive() - whenever(mockPrivacySettingsStore.privacyOn).thenReturn(false) + whenever(mockUserWhitelistDao.contains(any())).thenReturn(true) val site = site(url = "http://www.facebook.com", entity = TestEntity("Facebook", "Facebook", 9.0)) val value = testee.refreshCta(coroutineRule.testDispatcher, isBrowserShowing = true, site = site) diff --git a/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt b/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt index fbd0345db7a3..3ed1cc0ee0ff 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt @@ -20,6 +20,7 @@ import android.net.Uri import com.duckduckgo.app.httpsupgrade.api.HttpsBloomFilterFactory import com.duckduckgo.app.httpsupgrade.api.HttpsUpgradeService import com.duckduckgo.app.httpsupgrade.db.HttpsWhitelistDao +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.statistics.pixels.Pixel import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock @@ -38,6 +39,7 @@ class HttpsUpgraderTest { private var mockHttpsBloomFilterFactory: HttpsBloomFilterFactory = mock() private var mockWhitelistDao: HttpsWhitelistDao = mock() + private var mockUserWhitelistDao: UserWhitelistDao = mock() private var mockUpgradeService: HttpsUpgradeService = mock() private var mockServiceCall: Call> = mock() @@ -47,7 +49,7 @@ class HttpsUpgraderTest { @Before fun before() { whenever(mockHttpsBloomFilterFactory.create()).thenReturn(bloomFilter) - testee = HttpsUpgraderImpl(mockWhitelistDao, mockHttpsBloomFilterFactory, mockUpgradeService, mockPixel) + testee = HttpsUpgraderImpl(mockWhitelistDao, mockUserWhitelistDao, mockHttpsBloomFilterFactory, mockUpgradeService, mockPixel) testee.reloadData() } @@ -68,6 +70,13 @@ class HttpsUpgraderTest { assertFalse(testee.shouldUpgrade(Uri.parse("http://www.local.url"))) } + @Test + fun whenHttpDomainIsUserWhitelistedThenShouldNotUpgrade() { + whenever(mockUserWhitelistDao.contains("www.local.url")).thenReturn(true) + bloomFilter.add("www.local.url") + assertFalse(testee.shouldUpgrade(Uri.parse("http://www.local.url"))) + } + @Test fun whenHttpUriIsInLocalListAndInWhitelistThenShouldNotUpgrade() { bloomFilter.add("www.local.url") diff --git a/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModelTest.kt index 18c3e3aca2f4..8082a47ad458 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModelTest.kt @@ -17,22 +17,26 @@ package com.duckduckgo.app.privacy.ui import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao import com.duckduckgo.app.privacy.db.NetworkLeaderboardEntry +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.HttpsStatus import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.GOOD import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.UNKNOWN -import com.duckduckgo.app.privacy.store.PrivacySettingsStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.PRIVACY_DASHBOARD_OPENED +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.* import org.junit.Before @@ -45,15 +49,19 @@ class PrivacyDashboardViewModelTest { @Suppress("unused") var instantTaskExecutorRule = InstantTaskExecutorRule() + @ExperimentalCoroutinesApi + @get:Rule + var coroutineRule = CoroutineTestRule() + private var viewStateObserver: Observer = mock() - private var settingStore: PrivacySettingsStore = mock() + private var mockUserWhitelistDao: UserWhitelistDao = mock() private var networkLeaderboardDao: NetworkLeaderboardDao = mock() private var networkLeaderboardLiveData: LiveData> = mock() private var sitesVisitedLiveData: LiveData = mock() private var mockPixel: Pixel = mock() private val testee: PrivacyDashboardViewModel by lazy { - val model = PrivacyDashboardViewModel(settingStore, networkLeaderboardDao, mockPixel) + val model = PrivacyDashboardViewModel(mockUserWhitelistDao, networkLeaderboardDao, mockPixel, coroutineRule.testDispatcherProvider) model.viewState.observeForever(viewStateObserver) model } @@ -90,30 +98,28 @@ class PrivacyDashboardViewModelTest { } @Test - fun whenPrivacyInitiallyOnAndSwitchedOffThenShouldReloadIsTrue() { - whenever(settingStore.privacyOn) - .thenReturn(true) - .thenReturn(false) + fun whenSitePrivacySwitchedOffThenShouldReloadIsTrue() { + givenSiteWithPrivacyOn() + testee.onPrivacyToggled(false) assertTrue(testee.viewState.value!!.shouldReloadPage) } @Test - fun whenPrivacyInitiallyOnAndUnchangedThenShouldReloadIsFalse() { - whenever(settingStore.privacyOn).thenReturn(true) + fun whenSitePrivacyOffAndUnchangedThenShouldReloadIsFalse() { + givenSiteWithPrivacyOff() assertFalse(testee.viewState.value!!.shouldReloadPage) } @Test - fun whenPrivacyInitiallyOffAndSwitchedOnThenShouldReloadIsTrue() { - whenever(settingStore.privacyOn) - .thenReturn(false) - .thenReturn(true) + fun whenSitePrivacySwitchedOnThenShouldReloadIsTrue() { + givenSiteWithPrivacyOff() + testee.onPrivacyToggled(true) assertTrue(testee.viewState.value!!.shouldReloadPage) } @Test - fun whenPrivacyInitiallyOffAndUnchangedThenShouldReloadIsFalse() { - whenever(settingStore.privacyOn).thenReturn(false) + fun whenSitePrivacyOnAndUnchangedThenShouldReloadIsFalse() { + givenSiteWithPrivacyOn() assertFalse(testee.viewState.value!!.shouldReloadPage) } @@ -207,6 +213,16 @@ class PrivacyDashboardViewModelTest { assertFalse(viewState.shouldShowTrackerNetworkLeaderboard) } + private fun givenSiteWithPrivacyOn() { + whenever(mockUserWhitelistDao.contains(any())).thenReturn(false) + testee.onSiteChanged(site()) + } + + private fun givenSiteWithPrivacyOff() { + whenever(mockUserWhitelistDao.contains(any())).thenReturn(true) + testee.onSiteChanged(site()) + } + private fun site( https: HttpsStatus = HttpsStatus.SECURE, trackerCount: Int = 0, @@ -216,6 +232,7 @@ class PrivacyDashboardViewModelTest { improvedGrade: PrivacyGrade = PrivacyGrade.UNKNOWN ): Site { val site: Site = mock() + whenever(site.uri).thenReturn("https://example.com".toUri()) whenever(site.https).thenReturn(https) whenever(site.trackerCount).thenReturn(trackerCount) whenever(site.allTrackersBlocked).thenReturn(allTrackersBlocked) @@ -223,5 +240,4 @@ class PrivacyDashboardViewModelTest { whenever(site.calculateGrades()).thenReturn(Site.SiteGrades(grade, improvedGrade)) return site } - } \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/ScorecardViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/ScorecardViewModelTest.kt index 1cba4fdc7567..cb360b068284 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/ScorecardViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/ScorecardViewModelTest.kt @@ -18,19 +18,23 @@ package com.duckduckgo.app.privacy.ui import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Observer +import com.duckduckgo.app.CoroutineTestRule import com.duckduckgo.app.global.model.Site +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.HttpsStatus import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.PrivacyPractices.Practices import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.GOOD import com.duckduckgo.app.privacy.model.TestEntity -import com.duckduckgo.app.privacy.store.PrivacySettingsStore import com.duckduckgo.app.trackerdetection.model.Entity +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.* +import org.junit.Before import org.junit.Rule import org.junit.Test @@ -40,15 +44,24 @@ class ScorecardViewModelTest { @Suppress("unused") var instantTaskExecutorRule = InstantTaskExecutorRule() + @ExperimentalCoroutinesApi + @get:Rule + var coroutineRule = CoroutineTestRule() + private var viewStateObserver: Observer = mock() - private var settingStore: PrivacySettingsStore = mock() + private var userWhitelistDao: UserWhitelistDao = mock() private val testee: ScorecardViewModel by lazy { - val model = ScorecardViewModel(settingStore) + val model = ScorecardViewModel(userWhitelistDao, coroutineRule.testDispatcherProvider) model.viewState.observeForever(viewStateObserver) model } + @Before + fun before() { + whenever(userWhitelistDao.contains(any())).thenReturn(true) + } + @After fun after() { testee.viewState.removeObserver(viewStateObserver) diff --git a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorClientTypeTest.kt b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorClientTypeTest.kt index b86525158867..7740917b5339 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorClientTypeTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorClientTypeTest.kt @@ -16,7 +16,7 @@ package com.duckduckgo.app.trackerdetection -import com.duckduckgo.app.privacy.store.PrivacySettingsStore +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.trackerdetection.model.TrackingEvent import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.eq @@ -32,13 +32,13 @@ class TrackerDetectorClientTypeTest { private var mockEntityLookup: EntityLookup = mock() private var mockBlockingClient: Client = mock() private var mockWhitelistClient: Client = mock() - private var mockSettingStore: PrivacySettingsStore = mock() + private var mockUserWhitelistDao: UserWhitelistDao = mock() - private var testee = TrackerDetectorImpl(mockEntityLookup, mockSettingStore) + private var testee = TrackerDetectorImpl(mockEntityLookup, mockUserWhitelistDao) @Before fun before() { - whenever(mockSettingStore.privacyOn).thenReturn(true) + whenever(mockUserWhitelistDao.contains(any())).thenReturn(false) whenever(mockBlockingClient.matches(eq(Url.BLOCKED), any())).thenReturn(Client.Result(true)) whenever(mockBlockingClient.matches(eq(Url.BLOCKED_AND_WHITELISTED), any())).thenReturn(Client.Result(true)) diff --git a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorTest.kt b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorTest.kt index d0ed2bb02c85..2a2bbe1a1215 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/trackerdetection/TrackerDetectorTest.kt @@ -16,7 +16,7 @@ package com.duckduckgo.app.trackerdetection -import com.duckduckgo.app.privacy.store.PrivacySettingsStore +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.trackerdetection.Client.ClientName import com.duckduckgo.app.trackerdetection.Client.ClientName.EASYLIST import com.duckduckgo.app.trackerdetection.Client.ClientName.EASYPRIVACY @@ -30,8 +30,8 @@ import org.mockito.ArgumentMatchers.anyString class TrackerDetectorTest { private val mockEntityLookup: EntityLookup = mock() - private val settingStore: PrivacySettingsStore = mock() - private val trackerDetector = TrackerDetectorImpl(mockEntityLookup, settingStore) + private val mockUserWhitelistDao: UserWhitelistDao = mock() + private val trackerDetector = TrackerDetectorImpl(mockEntityLookup, mockUserWhitelistDao) @Test fun whenThereAreNoClientsThenClientCountIsZero() { @@ -75,8 +75,8 @@ class TrackerDetectorTest { } @Test - fun whenPrivacyOnAndAllClientsMatchThenEvaluateReturnsBlockedTrackingEvent() { - whenever(settingStore.privacyOn).thenReturn(true) + fun whenSiteIsNotUserWhitelistedAndAllClientsMatchThenEvaluateReturnsBlockedTrackingEvent() { + whenever(mockUserWhitelistDao.contains("example.com")).thenReturn(false) trackerDetector.addClient(alwaysMatchingClient(CLIENT_A)) trackerDetector.addClient(alwaysMatchingClient(CLIENT_B)) val expected = TrackingEvent("http://example.com/index.com", "http://thirdparty.com/update.js", null, null, true) @@ -85,8 +85,8 @@ class TrackerDetectorTest { } @Test - fun whenPrivacyOffAndAllClientsMatchThenEvaluateReturnsUnblockedTrackingEvent() { - whenever(settingStore.privacyOn).thenReturn(false) + fun whenSiteIsUserWhitelistedAndAllClientsMatchThenEvaluateReturnsUnblockedTrackingEvent() { + whenever(mockUserWhitelistDao.contains("example.com")).thenReturn(true) trackerDetector.addClient(alwaysMatchingClient(CLIENT_A)) trackerDetector.addClient(alwaysMatchingClient(CLIENT_B)) val expected = TrackingEvent("http://example.com/index.com", "http://thirdparty.com/update.js", null, null, false) @@ -95,8 +95,8 @@ class TrackerDetectorTest { } @Test - fun whenPrivacyOnAndSomeClientsMatchThenEvaluateReturnsBlockedTrackingEvent() { - whenever(settingStore.privacyOn).thenReturn(true) + fun whenSiteIsNotUserWhitelistedAndSomeClientsMatchThenEvaluateReturnsBlockedTrackingEvent() { + whenever(mockUserWhitelistDao.contains("example.com")).thenReturn(false) trackerDetector.addClient(neverMatchingClient(CLIENT_A)) trackerDetector.addClient(alwaysMatchingClient(CLIENT_B)) val expected = TrackingEvent("http://example.com/index.com", "http://thirdparty.com/update.js", null, null, true) @@ -105,8 +105,8 @@ class TrackerDetectorTest { } @Test - fun whenPrivacyOffAndSomeClientsMatchThenEvaluateReturnsUnblockedTrackingEvent() { - whenever(settingStore.privacyOn).thenReturn(false) + fun whenSiteIsUserWhitelistedAndSomeClientsMatchThenEvaluateReturnsUnblockedTrackingEvent() { + whenever(mockUserWhitelistDao.contains("example.com")).thenReturn(true) trackerDetector.addClient(neverMatchingClient(CLIENT_A)) trackerDetector.addClient(alwaysMatchingClient(CLIENT_B)) val expected = TrackingEvent("http://example.com/index.com", "http://thirdparty.com/update.js", null, null, false) 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 2567c5ccb70c..f02a32a0f1d3 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -79,7 +79,6 @@ import com.duckduckgo.app.global.device.DeviceInfo import com.duckduckgo.app.global.view.* import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.renderer.icon -import com.duckduckgo.app.privacy.store.PrivacySettingsStore import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.survey.model.Survey @@ -163,9 +162,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { @Inject lateinit var variantManager: VariantManager - @Inject - lateinit var privacySettingsStore: PrivacySettingsStore - val tabId get() = arguments!![TAB_ID_ARG] as String lateinit var userAgentProvider: UserAgentProvider @@ -1203,8 +1199,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { smoothProgressAnimator.onNewProgress(viewState.progress) { if (!viewState.isLoading) hide() } } - if (privacySettingsStore.privacyOn) { - + if (viewState.privacyOn) { if (lastSeenOmnibarViewState?.isEditing == true) { cancelAllAnimations() } 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 05850c553ff4..e60167d2a5b7 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -59,8 +59,10 @@ import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.global.* import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory +import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.domainMatchesUrl import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.api.StatisticsUpdater @@ -89,6 +91,7 @@ class BrowserTabViewModel( private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector, private val siteFactory: SiteFactory, private val tabRepository: TabRepository, + private val userWhitelistDao: UserWhitelistDao, private val networkLeaderboardDao: NetworkLeaderboardDao, private val bookmarksDao: BookmarksDao, private val autoComplete: AutoComplete, @@ -142,6 +145,7 @@ class BrowserTabViewModel( data class LoadingViewState( val isLoading: Boolean = false, + val privacyOn: Boolean = true, val progress: Int = 0 ) @@ -500,9 +504,19 @@ class BrowserTabViewModel( statisticsUpdater.refreshSearchRetentionAtb() } + updateLoadingStatePrivacy() + registerSiteVisit() } + private fun updateLoadingStatePrivacy() { + viewModelScope.launch(dispatchers.io()) { + site?.domain?.let { + loadingViewState.postValue(currentLoadingViewState().copy(privacyOn = !userWhitelistDao.contains(it))) + } + } + } + private fun urlUpdated(url: String) { Timber.v("Page url updated: $url") site?.url = url 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 9507418ad76b..f1c5c286cedb 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 @@ -26,11 +26,12 @@ import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.install.daysInstalled import com.duckduckgo.app.global.model.Site +import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.store.daxOnboardingActive -import com.duckduckgo.app.privacy.store.PrivacySettingsStore +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.Variant import com.duckduckgo.app.statistics.VariantManager @@ -52,10 +53,10 @@ class CtaViewModel @Inject constructor( private val surveyDao: SurveyDao, private val widgetCapabilities: WidgetCapabilities, private val dismissedCtaDao: DismissedCtaDao, + private val userWhitelistDao: UserWhitelistDao, private val variantManager: VariantManager, private val settingsDataStore: SettingsDataStore, private val onboardingStore: OnboardingStore, - private val settingsPrivacySettingsStore: PrivacySettingsStore, private val userStageStore: UserStageStore, private val dispatchers: DispatcherProvider ) { @@ -204,14 +205,13 @@ class CtaViewModel @Inject constructor( @WorkerThread private suspend fun canShowDaxCtaEndOfJourney(): Boolean = daxOnboardingActive() && - hasPrivacySettingsOn() && !daxDialogEndShown() && daxDialogIntroShown() && !settingsDataStore.hideTips && (daxDialogNetworkShown() || daxDialogOtherShown() || daxDialogSerpShown() || daxDialogTrackersFoundShown()) private suspend fun canShowDaxDialogCta(): Boolean { - if (!daxOnboardingActive() || settingsDataStore.hideTips || !hasPrivacySettingsOn()) { + if (!daxOnboardingActive() || settingsDataStore.hideTips) { return false } return true @@ -221,9 +221,13 @@ class CtaViewModel @Inject constructor( private fun getDaxDialogCta(site: Site?): Cta? { val nonNullSite = site ?: return null + val host = nonNullSite.domain + if (host != null && userWhitelistDao.contains(host)) { + return null + } + nonNullSite.let { // Is major network - val host = it.uri?.host if (it.entity != null && host != null) { it.entity?.let { entity -> if (!daxDialogNetworkShown() && DaxDialogCta.mainTrackerNetworks.contains(entity.displayName)) { @@ -259,8 +263,6 @@ class CtaViewModel @Inject constructor( .filterNotNull() .any() - private fun hasPrivacySettingsOn(): Boolean = settingsPrivacySettingsStore.privacyOn - private fun variant(): Variant = variantManager.getVariant() private fun daxDialogIntroShown(): Boolean = dismissedCtaDao.exists(CtaId.DAX_INTRO) diff --git a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt index 0ff94f2ef0bf..424ef2501f3d 100644 --- a/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/DaoModule.kt @@ -41,6 +41,9 @@ class DaoModule { @Provides fun providesTemporaryTrackingWhitelist(database: AppDatabase) = database.temporaryTrackingWhitelistDao() + @Provides + fun providesUserWhitelist(database: AppDatabase) = database.userWhitelistDao() + @Provides fun providesNetworkLeaderboardDao(database: AppDatabase) = database.networkLeaderboardDao() diff --git a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt index f9b378f7038b..c3d7d8aa2a5e 100644 --- a/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/StoreModule.kt @@ -40,9 +40,6 @@ abstract class StoreModule { @Binds abstract fun bindOnboardingStore(onboardingStore: OnboardingSharedPreferences): OnboardingStore - @Binds - abstract fun bindPrivacySettingsStore(privacySettingsStore: PrivacySettingsSharedPreferences): PrivacySettingsStore - @Binds abstract fun bindTermsOfServiceStore(termsOfServiceStore: TermsOfServiceRawStore): TermsOfServiceStore diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index f7469feac01d..e7dadab21a4a 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -45,15 +45,13 @@ import com.duckduckgo.app.icon.api.IconModifier import com.duckduckgo.app.icon.ui.ChangeIconViewModel import com.duckduckgo.app.launch.LaunchViewModel import com.duckduckgo.app.notification.AndroidNotificationScheduler -import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.ui.OnboardingPageManager import com.duckduckgo.app.onboarding.ui.OnboardingViewModel import com.duckduckgo.app.onboarding.ui.page.DefaultBrowserPageViewModel -import com.duckduckgo.app.onboarding.ui.page.TrackerBlockingSelectionViewModel import com.duckduckgo.app.playstore.PlayStoreUtils import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao -import com.duckduckgo.app.privacy.store.PrivacySettingsSharedPreferences +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel import com.duckduckgo.app.privacy.ui.PrivacyPracticesViewModel import com.duckduckgo.app.privacy.ui.ScorecardViewModel @@ -80,14 +78,13 @@ import javax.inject.Inject class ViewModelFactory @Inject constructor( private val statisticsUpdater: StatisticsUpdater, private val statisticsStore: StatisticsDataStore, - private val onboardingStore: OnboardingStore, private val userStageStore: UserStageStore, private val appInstallStore: AppInstallStore, private val queryUrlConverter: QueryUrlConverter, private val duckDuckGoUrlDetector: DuckDuckGoUrlDetector, private val tabRepository: TabRepository, - private val privacySettingsStore: PrivacySettingsSharedPreferences, private val siteFactory: SiteFactory, + private val userWhitelistDao: UserWhitelistDao, private val networkLeaderboardDao: NetworkLeaderboardDao, private val bookmarksDao: BookmarksDao, private val surveyDao: SurveyDao, @@ -127,7 +124,7 @@ class ViewModelFactory @Inject constructor( isAssignableFrom(BrowserTabViewModel::class.java) -> browserTabViewModel() isAssignableFrom(TabSwitcherViewModel::class.java) -> TabSwitcherViewModel(tabRepository, webViewSessionStorage) isAssignableFrom(PrivacyDashboardViewModel::class.java) -> privacyDashboardViewModel() - isAssignableFrom(ScorecardViewModel::class.java) -> ScorecardViewModel(privacySettingsStore) + isAssignableFrom(ScorecardViewModel::class.java) -> ScorecardViewModel(userWhitelistDao) isAssignableFrom(TrackerNetworksViewModel::class.java) -> TrackerNetworksViewModel() isAssignableFrom(PrivacyPracticesViewModel::class.java) -> PrivacyPracticesViewModel() isAssignableFrom(FeedbackViewModel::class.java) -> FeedbackViewModel(playStoreUtils, feedbackSubmitter) @@ -140,7 +137,6 @@ class ViewModelFactory @Inject constructor( isAssignableFrom(PositiveFeedbackLandingViewModel::class.java) -> PositiveFeedbackLandingViewModel() isAssignableFrom(ShareOpenEndedNegativeFeedbackViewModel::class.java) -> ShareOpenEndedNegativeFeedbackViewModel() isAssignableFrom(BrokenSiteNegativeFeedbackViewModel::class.java) -> BrokenSiteNegativeFeedbackViewModel() - isAssignableFrom(TrackerBlockingSelectionViewModel::class.java) -> TrackerBlockingSelectionViewModel(privacySettingsStore) isAssignableFrom(DefaultBrowserPageViewModel::class.java) -> defaultBrowserPage() isAssignableFrom(ChangeIconViewModel::class.java) -> changeAppIconViewModel() @@ -164,7 +160,7 @@ class ViewModelFactory @Inject constructor( private fun privacyDashboardViewModel(): PrivacyDashboardViewModel { return PrivacyDashboardViewModel( - privacySettingsStore, + userWhitelistDao, networkLeaderboardDao, pixel ) @@ -186,6 +182,7 @@ class ViewModelFactory @Inject constructor( duckDuckGoUrlDetector = duckDuckGoUrlDetector, siteFactory = siteFactory, tabRepository = tabRepository, + userWhitelistDao = userWhitelistDao, networkLeaderboardDao = networkLeaderboardDao, bookmarksDao = bookmarksDao, autoComplete = autoCompleteApi, diff --git a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt index 73c2b9faf239..e3203f9e0c39 100644 --- a/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt +++ b/app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt @@ -40,11 +40,9 @@ import com.duckduckgo.app.httpsupgrade.model.HttpsWhitelistedDomain import com.duckduckgo.app.notification.db.NotificationDao import com.duckduckgo.app.notification.model.Notification import com.duckduckgo.app.onboarding.store.* -import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao -import com.duckduckgo.app.privacy.db.NetworkLeaderboardEntry -import com.duckduckgo.app.privacy.db.PrivacyProtectionCountDao -import com.duckduckgo.app.privacy.db.SitesVisitedEntity +import com.duckduckgo.app.privacy.db.* import com.duckduckgo.app.privacy.model.PrivacyProtectionCountsEntity +import com.duckduckgo.app.privacy.model.UserWhitelistedDomain import com.duckduckgo.app.survey.db.SurveyDao import com.duckduckgo.app.survey.model.Survey import com.duckduckgo.app.tabs.db.TabsDao @@ -58,11 +56,12 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.usage.search.SearchCountEntity @Database( - exportSchema = true, version = 18, entities = [ + exportSchema = true, version = 19, entities = [ TdsTracker::class, TdsEntity::class, TdsDomainEntity::class, TemporaryTrackingWhitelistedDomain::class, + UserWhitelistedDomain::class, HttpsBloomFilterSpec::class, HttpsWhitelistedDomain::class, NetworkLeaderboardEntry::class, @@ -100,6 +99,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun tdsEntityDao(): TdsEntityDao abstract fun tdsDomainEntityDao(): TdsDomainEntityDao abstract fun temporaryTrackingWhitelistDao(): TemporaryTrackingWhitelistDao + abstract fun userWhitelistDao(): UserWhitelistDao abstract fun httpsWhitelistedDao(): HttpsWhitelistDao abstract fun httpsBloomFilterSpecDao(): HttpsBloomFilterSpecDao abstract fun networkLeaderboardDao(): NetworkLeaderboardDao @@ -269,6 +269,12 @@ class MigrationsProvider(val context: Context) { } } + val MIGRATION_18_TO_19: Migration = object : Migration(18, 19) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `user_whitelist` (`domain` TEXT NOT NULL, PRIMARY KEY(`domain`))") + } + } + val ALL_MIGRATIONS: List get() = listOf( MIGRATION_1_TO_2, @@ -287,7 +293,8 @@ class MigrationsProvider(val context: Context) { MIGRATION_14_TO_15, MIGRATION_15_TO_16, MIGRATION_16_TO_17, - MIGRATION_17_TO_18 + MIGRATION_17_TO_18, + MIGRATION_18_TO_19 ) @Deprecated( diff --git a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt index a76213c1914b..35241425ac39 100644 --- a/app/src/main/java/com/duckduckgo/app/global/model/Site.kt +++ b/app/src/main/java/com/duckduckgo/app/global/model/Site.kt @@ -65,4 +65,6 @@ interface Site { fun Site.domainMatchesUrl(matchingUrl: String): Boolean { return uri?.baseHost == matchingUrl.toUri().baseHost -} \ No newline at end of file +} + +val Site.domain get() = uri?.host \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/httpsupgrade/HttpsUpgrader.kt b/app/src/main/java/com/duckduckgo/app/httpsupgrade/HttpsUpgrader.kt index 61b24777b50b..5eeb1afac529 100644 --- a/app/src/main/java/com/duckduckgo/app/httpsupgrade/HttpsUpgrader.kt +++ b/app/src/main/java/com/duckduckgo/app/httpsupgrade/HttpsUpgrader.kt @@ -25,6 +25,7 @@ import com.duckduckgo.app.global.toHttps import com.duckduckgo.app.httpsupgrade.api.HttpsBloomFilterFactory import com.duckduckgo.app.httpsupgrade.api.HttpsUpgradeService import com.duckduckgo.app.httpsupgrade.db.HttpsWhitelistDao +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* import timber.log.Timber @@ -44,7 +45,8 @@ interface HttpsUpgrader { } class HttpsUpgraderImpl( - private val whitelistedDao: HttpsWhitelistDao, + private val httpsWhitelistDao: HttpsWhitelistDao, + private val userWhitelistDao: UserWhitelistDao, private val bloomFactory: HttpsBloomFilterFactory, private val httpsUpgradeService: HttpsUpgradeService, private val pixel: Pixel @@ -68,9 +70,15 @@ class HttpsUpgraderImpl( return false } - if (whitelistedDao.contains(host)) { + if (userWhitelistDao.contains(host)) { pixel.fire(HTTPS_NO_LOOKUP) - Timber.d("$host is in whitelist and so not upgradable") + Timber.d("$host is in user whitelist and so not upgradable") + return false + } + + if (httpsWhitelistDao.contains(host)) { + pixel.fire(HTTPS_NO_LOOKUP) + Timber.d("$host is in https whitelist and so not upgradable") return false } diff --git a/app/src/main/java/com/duckduckgo/app/httpsupgrade/di/HttpsUpgraderModule.kt b/app/src/main/java/com/duckduckgo/app/httpsupgrade/di/HttpsUpgraderModule.kt index f51a1a198ad7..b360b0dee927 100644 --- a/app/src/main/java/com/duckduckgo/app/httpsupgrade/di/HttpsUpgraderModule.kt +++ b/app/src/main/java/com/duckduckgo/app/httpsupgrade/di/HttpsUpgraderModule.kt @@ -24,6 +24,7 @@ import com.duckduckgo.app.httpsupgrade.api.HttpsBloomFilterFactoryImpl import com.duckduckgo.app.httpsupgrade.api.HttpsUpgradeService import com.duckduckgo.app.httpsupgrade.db.HttpsBloomFilterSpecDao import com.duckduckgo.app.httpsupgrade.db.HttpsWhitelistDao +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.statistics.pixels.Pixel import dagger.Module import dagger.Provides @@ -36,11 +37,12 @@ class HttpsUpgraderModule { @Provides fun httpsUpgrader( whitelistDao: HttpsWhitelistDao, + userWhitelistDao: UserWhitelistDao, bloomFilterFactory: HttpsBloomFilterFactory, httpsUpgradeService: HttpsUpgradeService, pixel: Pixel ): HttpsUpgrader { - return HttpsUpgraderImpl(whitelistDao, bloomFilterFactory, httpsUpgradeService, pixel) + return HttpsUpgraderImpl(whitelistDao, userWhitelistDao, bloomFilterFactory, httpsUpgradeService, pixel) } @Provides diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/TrackerBlockingSelectionViewModel.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/TrackerBlockingSelectionViewModel.kt deleted file mode 100644 index 8ca36fc392fb..000000000000 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/TrackerBlockingSelectionViewModel.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2019 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.onboarding.ui.page - -import androidx.lifecycle.ViewModel -import com.duckduckgo.app.privacy.store.PrivacySettingsStore - - -class TrackerBlockingSelectionViewModel(private val settingsStore: PrivacySettingsStore) : ViewModel() { - - fun onTrackerBlockingEnabled() { - settingsStore.privacyOn = true - } - - fun onTrackerBlockingDisabled() { - settingsStore.privacyOn = false - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/privacy/db/UserWhitelistDao.kt b/app/src/main/java/com/duckduckgo/app/privacy/db/UserWhitelistDao.kt new file mode 100644 index 000000000000..ceae950f9db2 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/privacy/db/UserWhitelistDao.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 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.privacy.db + +import androidx.core.net.toUri +import androidx.room.* +import com.duckduckgo.app.privacy.model.UserWhitelistedDomain + +@Dao +abstract class UserWhitelistDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun insert(domain: UserWhitelistedDomain) + + fun insert(domain: String) { + insert(UserWhitelistedDomain(domain)) + } + + @Delete + abstract fun delete(domain: UserWhitelistedDomain) + + fun delete(domain: String) { + delete(UserWhitelistedDomain(domain)) + } + + @Query("select count(1) > 0 from user_whitelist where domain = :domain") + abstract fun contains(domain: String): Boolean +} diff --git a/app/src/main/java/com/duckduckgo/app/privacy/store/PrivacySettingsStore.kt b/app/src/main/java/com/duckduckgo/app/privacy/model/UserWhitelistedDomain.kt similarity index 64% rename from app/src/main/java/com/duckduckgo/app/privacy/store/PrivacySettingsStore.kt rename to app/src/main/java/com/duckduckgo/app/privacy/model/UserWhitelistedDomain.kt index 478bfb0db544..2185c58cb2ac 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/store/PrivacySettingsStore.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/model/UserWhitelistedDomain.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 DuckDuckGo + * Copyright (c) 2020 DuckDuckGo * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,8 +14,13 @@ * limitations under the License. */ -package com.duckduckgo.app.privacy.store +package com.duckduckgo.app.privacy.model -interface PrivacySettingsStore { - var privacyOn: Boolean -} \ No newline at end of file +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.duckduckgo.app.trackerdetection.model.DomainContainer + +@Entity(tableName = "user_whitelist") +data class UserWhitelistedDomain( + @PrimaryKey val domain: String +) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/store/PrivacySettingsSharedPreferences.kt b/app/src/main/java/com/duckduckgo/app/privacy/store/PrivacySettingsSharedPreferences.kt deleted file mode 100644 index b56bae41b439..000000000000 --- a/app/src/main/java/com/duckduckgo/app/privacy/store/PrivacySettingsSharedPreferences.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2017 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.privacy.store - -import android.content.Context -import android.content.Context.MODE_PRIVATE -import android.content.SharedPreferences -import javax.inject.Inject - -class PrivacySettingsSharedPreferences @Inject constructor(private val context: Context) : PrivacySettingsStore { - - override var privacyOn: Boolean - get() = preferences.getBoolean(KEY_PRIVACY_ON, true) - set(on) { - val editor = preferences.edit() - editor.putBoolean(KEY_PRIVACY_ON, on) - editor.apply() - } - - private val preferences: SharedPreferences - get() = context.getSharedPreferences(FILENAME, MODE_PRIVATE) - - - companion object { - private const val FILENAME = "com.duckduckgo.app.privacymonitor.settings" - private const val KEY_PRIVACY_ON = "com.duckduckgo.app.privacymonitor.privacyon" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt index bc0686f7856d..8b1b03be090b 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt @@ -57,7 +57,7 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { setContentView(R.layout.activity_privacy_dashboard) configureToolbar() - viewModel.viewState.observe(this, Observer { + viewModel.viewState.observe(this, Observer { it?.let { render(it) } }) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt index bbe0b45638fd..1017185a1340 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt @@ -21,21 +21,27 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel +import com.duckduckgo.app.global.DefaultDispatcherProvider +import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.model.Site +import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao import com.duckduckgo.app.privacy.db.NetworkLeaderboardEntry +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.HttpsStatus import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.UNKNOWN -import com.duckduckgo.app.privacy.store.PrivacySettingsStore import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch class PrivacyDashboardViewModel( - private val settingsStore: PrivacySettingsStore, + private val userWhitelistDao: UserWhitelistDao, networkLeaderboardDao: NetworkLeaderboardDao, - private val pixel: Pixel + private val pixel: Pixel, + private val dispatchers: DispatcherProvider = DefaultDispatcherProvider() ) : ViewModel() { data class ViewState( @@ -61,11 +67,6 @@ class PrivacyDashboardViewModel( private val trackerNetworkLeaderboard: LiveData> = networkLeaderboardDao.trackerNetworkLeaderboard() private val trackerNetworkActivityObserver = Observer> { onTrackerNetworkEntriesChanged(it) } - private val privacyInitiallyOn = settingsStore.privacyOn - - private val shouldReloadPage: Boolean - get() = privacyInitiallyOn != settingsStore.privacyOn - init { pixel.fire(PRIVACY_DASHBOARD_OPENED) resetViewState() @@ -119,40 +120,51 @@ class PrivacyDashboardViewModel( httpsStatus = HttpsStatus.SECURE, trackerCount = 0, allTrackersBlocked = true, - toggleEnabled = settingsStore.privacyOn, + toggleEnabled = true, practices = UNKNOWN, shouldShowTrackerNetworkLeaderboard = false, sitesVisited = 0, trackerNetworkEntries = emptyList(), - shouldReloadPage = shouldReloadPage + shouldReloadPage = false ) } private fun updateSite(site: Site) { val grades = site.calculateGrades() - viewState.value = viewState.value?.copy( - domain = site.uri?.host ?: "", - beforeGrade = grades.grade, - afterGrade = grades.improvedGrade, - httpsStatus = site.https, - trackerCount = site.trackerCount, - allTrackersBlocked = site.allTrackersBlocked, - practices = site.privacyPractices.summary - ) + GlobalScope.launch(dispatchers.io()) { + viewState.postValue(viewState.value?.copy( + domain = site.domain ?: "", + beforeGrade = grades.grade, + afterGrade = grades.improvedGrade, + httpsStatus = site.https, + trackerCount = site.trackerCount, + allTrackersBlocked = site.allTrackersBlocked, + toggleEnabled = site.domain?.let { !userWhitelistDao.contains(it) } ?: true, + practices = site.privacyPractices.summary + )) + } } fun onPrivacyToggled(enabled: Boolean) { - if (enabled != viewState.value?.toggleEnabled) { + if (enabled == viewState.value?.toggleEnabled) { + return + } - settingsStore.privacyOn = enabled - val pixelName = if (enabled) TRACKER_BLOCKER_DASHBOARD_TURNED_ON else TRACKER_BLOCKER_DASHBOARD_TURNED_OFF - pixel.fire(pixelName) + viewState.value = viewState.value?.copy( + toggleEnabled = enabled, + shouldReloadPage = true + ) - viewState.value = viewState.value?.copy( - toggleEnabled = enabled, - shouldReloadPage = shouldReloadPage - ) + val domain = site?.domain ?: return + GlobalScope.launch(dispatchers.io()) { + if (enabled) { + userWhitelistDao.delete(domain) + pixel.fire(TRACKER_BLOCKER_DASHBOARD_TURNED_ON) + } else { + userWhitelistDao.insert(domain) + pixel.fire(TRACKER_BLOCKER_DASHBOARD_TURNED_OFF) + } } } diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyPracticesViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyPracticesViewModel.kt index ea4c133e0bad..0bb546c3c136 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyPracticesViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyPracticesViewModel.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.privacy.ui import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.duckduckgo.app.global.model.Site +import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.UNKNOWN @@ -52,7 +53,7 @@ class PrivacyPracticesViewModel : ViewModel() { return } viewState.value = viewState.value?.copy( - domain = site.uri?.host ?: "", + domain = site.domain ?: "", practices = site.privacyPractices.summary, goodTerms = site.privacyPractices.goodReasons, badTerms = site.privacyPractices.badReasons diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt index 7b59c93130e1..b101953366cf 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt @@ -16,16 +16,25 @@ package com.duckduckgo.app.privacy.ui +import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.duckduckgo.app.global.DefaultDispatcherProvider +import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.model.Site +import com.duckduckgo.app.global.model.domain +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.HttpsStatus import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.UNKNOWN -import com.duckduckgo.app.privacy.store.PrivacySettingsStore +import kotlinx.coroutines.launch -class ScorecardViewModel(private val settingsStore: PrivacySettingsStore) : ViewModel() { +class ScorecardViewModel( + private val userWhitelistDao: UserWhitelistDao, + private val dispatchers: DispatcherProvider = DefaultDispatcherProvider() +) : ViewModel() { data class ViewState( val domain: String, @@ -67,28 +76,35 @@ class ScorecardViewModel(private val settingsStore: PrivacySettingsStore) : View majorNetworkCount = 0, allTrackersBlocked = true, practices = UNKNOWN, - privacyOn = settingsStore.privacyOn, + privacyOn = true, showIsMemberOfMajorNetwork = false, showEnhancedGrade = false ) } + @WorkerThread private fun updateSite(site: Site) { + val domain = site.domain ?: "" val grades = site.calculateGrades() val grade = grades.grade val improvedGrade = grades.improvedGrade - viewState.value = viewState.value?.copy( - domain = site.uri?.host ?: "", - beforeGrade = grade, - afterGrade = improvedGrade, - trackerCount = site.trackerCount, - majorNetworkCount = site.majorNetworkCount, - httpsStatus = site.https, - allTrackersBlocked = site.allTrackersBlocked, - practices = site.privacyPractices.summary, - showIsMemberOfMajorNetwork = site.entity?.isMajor ?: false, - showEnhancedGrade = grade != improvedGrade - ) + viewModelScope.launch(dispatchers.io()) { + viewState.postValue( + viewState.value?.copy( + domain = domain, + beforeGrade = grade, + afterGrade = improvedGrade, + trackerCount = site.trackerCount, + majorNetworkCount = site.majorNetworkCount, + httpsStatus = site.https, + allTrackersBlocked = site.allTrackersBlocked, + practices = site.privacyPractices.summary, + privacyOn = !userWhitelistDao.contains(domain), + showIsMemberOfMajorNetwork = site.entity?.isMajor ?: false, + showEnhancedGrade = grade != improvedGrade + ) + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksViewModel.kt index a5b9f7282981..adab1f5966c4 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/TrackerNetworksViewModel.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.duckduckgo.app.global.baseHost import com.duckduckgo.app.global.model.Site +import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.trackerdetection.model.Entity import com.duckduckgo.app.trackerdetection.model.TdsEntity import com.duckduckgo.app.trackerdetection.model.TrackingEvent @@ -56,7 +57,7 @@ class TrackerNetworksViewModel : ViewModel() { return } viewState.value = viewState.value?.copy( - domain = site.uri?.host ?: "", + domain = site.domain ?: "", trackerCount = site.trackerCount, allTrackersBlocked = site.allTrackersBlocked, trackingEventsByNetwork = distinctTrackersByEntity(site.trackingEvents) diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt index 43a6014137a8..f63036073aa4 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt @@ -27,6 +27,7 @@ interface Client { // current clients TDS(ClientType.BLOCKING), TEMPORARY_WHITELIST(ClientType.WHITELIST), + USER_WHITELIST(ClientType.WHITELIST), // legacy clients EASYLIST(ClientType.BLOCKING), diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDataLoader.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDataLoader.kt index 3c2f17e2e08d..2bb8d9762e3d 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDataLoader.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDataLoader.kt @@ -21,11 +21,7 @@ import androidx.annotation.WorkerThread import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.trackerdetection.api.TdsJson -import com.duckduckgo.app.trackerdetection.db.TdsMetadataDao -import com.duckduckgo.app.trackerdetection.db.TdsDomainEntityDao -import com.duckduckgo.app.trackerdetection.db.TdsEntityDao -import com.duckduckgo.app.trackerdetection.db.TdsTrackerDao -import com.duckduckgo.app.trackerdetection.db.TemporaryTrackingWhitelistDao +import com.duckduckgo.app.trackerdetection.db.* import com.duckduckgo.app.trackerdetection.model.TdsMetadata import com.squareup.moshi.Moshi import timber.log.Timber diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt index 2d42200c6763..70b39961c8f9 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/TrackerDetector.kt @@ -16,8 +16,9 @@ package com.duckduckgo.app.trackerdetection +import androidx.core.net.toUri import com.duckduckgo.app.global.UriString.Companion.sameOrSubdomain -import com.duckduckgo.app.privacy.store.PrivacySettingsStore +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.trackerdetection.model.TrackingEvent import timber.log.Timber import java.util.concurrent.CopyOnWriteArrayList @@ -29,7 +30,7 @@ interface TrackerDetector { class TrackerDetectorImpl( private val entityLookup: EntityLookup, - private val settings: PrivacySettingsStore + private val userWhitelistDao: UserWhitelistDao ) : TrackerDetector { private val clients = CopyOnWriteArrayList() @@ -62,7 +63,7 @@ class TrackerDetectorImpl( if (result != null) { Timber.v("$documentUrl resource $url WAS identified as a tracker") val entity = if (result.entityName != null) entityLookup.entityForName(result.entityName) else null - return TrackingEvent(documentUrl, url, result.categories, entity, settings.privacyOn) + return TrackingEvent(documentUrl, url, result.categories, entity, !userWhitelistDao.isDocumentWhitelisted(documentUrl)) } Timber.v("$documentUrl resource $url was not identified as a tracker") @@ -83,4 +84,11 @@ class TrackerDetectorImpl( } val clientCount get() = clients.count() +} + +private fun UserWhitelistDao.isDocumentWhitelisted(document: String?): Boolean { + document?.toUri()?.host?.let { + return contains(it) + } + return false } \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/di/TrackerDetectionModule.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/di/TrackerDetectionModule.kt index d9df02dc2158..b34d2b72a520 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/di/TrackerDetectionModule.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/di/TrackerDetectionModule.kt @@ -16,7 +16,7 @@ package com.duckduckgo.app.trackerdetection.di -import com.duckduckgo.app.privacy.store.PrivacySettingsStore +import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.trackerdetection.EntityLookup import com.duckduckgo.app.trackerdetection.TrackerDetector import com.duckduckgo.app.trackerdetection.TrackerDetectorImpl @@ -30,7 +30,7 @@ class TrackerDetectionModule { @Provides @Singleton - fun trackerDetector(entityLookup: EntityLookup, settings: PrivacySettingsStore): TrackerDetector { - return TrackerDetectorImpl(entityLookup, settings) + fun trackerDetector(entityLookup: EntityLookup, userWhitelistDao: UserWhitelistDao): TrackerDetector { + return TrackerDetectorImpl(entityLookup, userWhitelistDao) } } \ No newline at end of file From 263c9f3ad9e1994ae9b8482c1450733efe47df6b Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 24 Apr 2020 10:43:52 +0100 Subject: [PATCH 02/15] Update pixels --- .../duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt | 4 ++-- .../main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt index 1017185a1340..5718aa2a7be0 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt @@ -160,10 +160,10 @@ class PrivacyDashboardViewModel( GlobalScope.launch(dispatchers.io()) { if (enabled) { userWhitelistDao.delete(domain) - pixel.fire(TRACKER_BLOCKER_DASHBOARD_TURNED_ON) + pixel.fire(PRIVACY_DASHBOARD_WHITELIST_REMOVE) } else { userWhitelistDao.insert(domain) - pixel.fire(TRACKER_BLOCKER_DASHBOARD_TURNED_OFF) + pixel.fire(PRIVACY_DASHBOARD_WHITELIST_ADD) } } } diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 4f6052cda94a..ce9faf8ce0a3 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -70,6 +70,8 @@ interface Pixel { PRIVACY_DASHBOARD_GLOBAL_STATS("mp_s"), PRIVACY_DASHBOARD_PRIVACY_PRACTICES("mp_p"), PRIVACY_DASHBOARD_NETWORKS("mp_n"), + PRIVACY_DASHBOARD_WHITELIST_ADD("mp_wla"), + PRIVACY_DASHBOARD_WHITELIST_REMOVE("mp_wlr"), HTTPS_NO_LOOKUP("m_https_nl"), HTTPS_LOCAL_UPGRADE("m_https_lu"), @@ -78,9 +80,6 @@ interface Pixel { HTTPS_SERVICE_REQUEST_NO_UPGRADE("m_https_srn"), HTTPS_SERVICE_CACHE_NO_UPGRADE("m_https_scn"), - TRACKER_BLOCKER_DASHBOARD_TURNED_ON(pixelName = "m_tb_on_pd"), - TRACKER_BLOCKER_DASHBOARD_TURNED_OFF(pixelName = "m_tb_off_pd"), - DEFAULT_BROWSER_SET("m_db_s"), DEFAULT_BROWSER_NOT_SET("m_db_ns"), DEFAULT_BROWSER_UNSET("m_db_u"), From 94398b68b03c707cf6e4f1d35ab929b44a5e4c28 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 24 Apr 2020 10:44:44 +0100 Subject: [PATCH 03/15] New buttons --- .../res/layout/content_privacy_dashboard.xml | 52 ++++++++++++++++++- .../main/res/values/string-untranslated.xml | 6 +++ app/src/main/res/values/strings.xml | 1 - 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/content_privacy_dashboard.xml b/app/src/main/res/layout/content_privacy_dashboard.xml index 23a81d865f4e..c245c7de2681 100644 --- a/app/src/main/res/layout/content_privacy_dashboard.xml +++ b/app/src/main/res/layout/content_privacy_dashboard.xml @@ -47,6 +47,7 @@ android:id="@+id/httpsContainer" android:layout_width="match_parent" android:layout_height="wrap_content" + app:layout_constraintBottom_toTopOf="@id/networksContainer" android:onClick="onEncryptionClicked"> @@ -139,6 +141,54 @@ + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/buttonContainer"> Remove Search + + Site Privacy Protection + + + Manage Whitelist + Report Broken Site diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 062a34e7b8a6..185c95477f7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -124,7 +124,6 @@ Privacy Practices results from ToS;DR Good Practice Icon Bad Practice Icon - Privacy Protection TRACKER NETWORK TOP OFFENDERS We\'re still collecting data to show how many trackers we\'ve blocked. From 2ccff7d524c77066425c17dfb0c85e4b1979e065 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Thu, 30 Apr 2020 10:04:37 +0100 Subject: [PATCH 04/15] Add whitelist --- .../bookmarks/ui/BookmarksViewModelTest.kt | 8 +- .../app/brokensite/BrokenSiteDataTest.kt | 47 ++++ .../app/browser/BrowserTabViewModelTest.kt | 109 +------- .../feedback/ui/BrokenSiteViewModelTest.kt | 2 - .../app/privacy/db/UserWhitelistDaoTest.kt | 90 +++++++ .../ui/PrivacyDashboardViewModelTest.kt | 30 ++- .../app/settings/SettingsViewModelTest.kt | 7 + app/src/main/AndroidManifest.xml | 4 + .../app/bookmarks/ui/BookmarksActivity.kt | 8 +- .../ui/EditBookmarkDialogFragment.kt | 2 +- .../app/brokensite/BrokenSiteActivity.kt | 10 +- .../app/brokensite/BrokenSiteData.kt | 40 +++ .../app/browser/BrowserTabFragment.kt | 8 +- .../app/browser/BrowserTabViewModel.kt | 11 +- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 6 +- .../duckduckgo/app/di/AndroidBindingModule.kt | 9 +- .../duckduckgo/app/global/ViewModelFactory.kt | 6 +- .../app/privacy/db/UserWhitelistDao.kt | 5 +- .../privacy/ui/PrivacyDashboardActivity.kt | 49 +++- .../privacy/ui/PrivacyDashboardViewModel.kt | 20 ++ .../app/privacy/ui/WhitelistActivity.kt | 237 ++++++++++++++++++ .../app/privacy/ui/WhitelistViewModel.kt | 100 ++++++++ .../app/settings/SettingsActivity.kt | 35 +-- .../app/settings/SettingsViewModel.kt | 6 + .../duckduckgo/app/statistics/pixels/Pixel.kt | 5 +- ... => ic_overflow_recycler_entries_24dp.xml} | 0 .../main/res/layout/activity_whitelist.xml | 67 +++++ .../res/layout/content_privacy_dashboard.xml | 34 +-- .../res/layout/content_settings_privacy.xml | 9 + app/src/main/res/layout/edit_whitelist.xml | 37 +++ .../main/res/layout/view_bookmark_entry.xml | 2 +- .../main/res/layout/view_whitelist_entry.xml | 65 +++++ .../main/res/menu/whitelist_activity_menu.xml | 28 +++ .../whitelist_individual_overflow_menu.xml | 29 +++ app/src/main/res/values-bg/strings.xml | 4 +- app/src/main/res/values-cs/strings.xml | 4 +- app/src/main/res/values-da/strings.xml | 4 +- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-el/strings.xml | 4 +- app/src/main/res/values-es/strings.xml | 4 +- app/src/main/res/values-et/strings.xml | 4 +- app/src/main/res/values-fi/strings.xml | 4 +- app/src/main/res/values-fr/strings.xml | 4 +- app/src/main/res/values-hr/strings.xml | 4 +- app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-it/strings.xml | 4 +- app/src/main/res/values-lt/strings.xml | 4 +- app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-nb/strings.xml | 4 +- app/src/main/res/values-nl/strings.xml | 4 +- app/src/main/res/values-pl/strings.xml | 4 +- app/src/main/res/values-pt/strings.xml | 4 +- app/src/main/res/values-ro/strings.xml | 4 +- app/src/main/res/values-ru/strings.xml | 4 +- app/src/main/res/values-sk/strings.xml | 4 +- app/src/main/res/values-sl/strings.xml | 4 +- app/src/main/res/values-sv/strings.xml | 4 +- app/src/main/res/values-tr/strings.xml | 4 +- app/src/main/res/values/attrs.xml | 1 + .../main/res/values/string-untranslated.xml | 21 +- app/src/main/res/values/strings.xml | 10 +- app/src/main/res/values/themes.xml | 2 + 62 files changed, 1006 insertions(+), 249 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt create mode 100644 app/src/androidTest/java/com/duckduckgo/app/privacy/db/UserWhitelistDaoTest.kt create mode 100644 app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteData.kt create mode 100644 app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt create mode 100644 app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt rename app/src/main/res/drawable/{ic_overflow_bookmarks_24dp.xml => ic_overflow_recycler_entries_24dp.xml} (100%) create mode 100644 app/src/main/res/layout/activity_whitelist.xml create mode 100644 app/src/main/res/layout/edit_whitelist.xml create mode 100644 app/src/main/res/layout/view_whitelist_entry.xml create mode 100644 app/src/main/res/menu/whitelist_activity_menu.xml create mode 100644 app/src/main/res/menu/whitelist_individual_overflow_menu.xml diff --git a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt index ebbca94a3ee4..c5c7b7a568c2 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/bookmarks/ui/BookmarksViewModelTest.kt @@ -25,6 +25,7 @@ import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever +import org.junit.After import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before @@ -62,6 +63,12 @@ class BookmarksViewModelTest { whenever(bookmarksDao.bookmarks()).thenReturn(liveData) } + @After + fun after() { + testee.viewState.removeObserver(viewStateObserver) + testee.command.removeObserver(commandObserver) + } + @Test fun whenBookmarkDeletedThenDaoUpdated() { testee.delete(bookmark) @@ -94,5 +101,4 @@ class BookmarksViewModelTest { assertNotNull(captor.value) assertNotNull(captor.value.bookmarks) } - } \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt b/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt new file mode 100644 index 000000000000..991e29315217 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 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.brokensite + +import com.duckduckgo.app.global.model.Site +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever +import org.junit.Assert.assertEquals +import org.junit.Test + +class BrokenSiteDataTest { + + @Test + fun whenSiteIsNotNullThenBrokenSiteDataContainsUrl() { + val site = buildSite("foo.com") + val data = BrokenSiteData.fromSite(site) + assertEquals("foo.com", data.url) + } + + @Test + fun whenSiteIsNullThenBrokenSiteDataContainsBlankUrl() { + val data = BrokenSiteData.fromSite(null) + assertEquals("", data.url) + } + + private fun buildSite( + url: String + ): Site { + val site: Site = mock() + whenever(site.url).thenReturn(url) + return site + } +} \ No newline at end of file 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 aa9f97295354..4c36b1be3f55 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -1070,14 +1070,14 @@ class BrowserTabViewModelTest { loadUrl("foo.com", isBrowserShowing = true) testee.onBrokenSiteSelected() val command = captureCommands().value as Command.BrokenSiteFeedback - assertEquals("foo.com", command.url) + assertEquals("foo.com", command.data.url) } @Test fun whenNoSiteAndBrokenSiteSelectedThenBrokenSiteFeedbackCommandSentWithoutUrl() { testee.onBrokenSiteSelected() val command = captureCommands().value as Command.BrokenSiteFeedback - assertEquals("", command.url) + assertEquals("", command.data.url) } @Test @@ -1560,111 +1560,6 @@ class BrowserTabViewModelTest { assertCommandIssued() } - @Test - fun whenOnBrokenSiteSelectedAndNoHttpsUpgradedThenReturnHttpsUpgradedFalse() { - testee.onBrokenSiteSelected() - - val command = captureCommands().lastValue - assertTrue(command is Command.BrokenSiteFeedback) - - val brokenSiteFeedback = command as Command.BrokenSiteFeedback - assertFalse(brokenSiteFeedback.httpsUpgraded) - } - - @Test - fun whenOnBrokenSiteSelectedAndNoTrackersThenReturnBlockedTrackersEmptyString() { - givenOneActiveTabSelected() - - testee.onBrokenSiteSelected() - - val command = captureCommands().lastValue - assertTrue(command is Command.BrokenSiteFeedback) - - val brokenSiteFeedback = command as Command.BrokenSiteFeedback - assertEquals("", brokenSiteFeedback.blockedTrackers) - } - - @Test - fun whenOnBrokenSiteSelectedAndTrackersBlockedThenReturnBlockedTrackers() { - givenOneActiveTabSelected() - val event = TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", emptyList(), null, false) - val anotherEvent = TrackingEvent("http://www.example.com/test", "http://www.anothertracker.com/tracker.js", emptyList(), null, false) - - testee.trackerDetected(event) - testee.trackerDetected(anotherEvent) - testee.onBrokenSiteSelected() - - val command = captureCommands().lastValue - assertTrue(command is Command.BrokenSiteFeedback) - - val brokenSiteFeedback = command as Command.BrokenSiteFeedback - assertEquals("www.tracker.com,www.anothertracker.com", brokenSiteFeedback.blockedTrackers) - } - - @Test - fun whenOnBrokenSiteSelectedAndSameHostTrackersBlockedThenDoNotReturnDuplicatedBlockedTrackers() { - givenOneActiveTabSelected() - val event = TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", emptyList(), null, false) - val anotherEvent = TrackingEvent("http://www.example.com/test", "http://www.tracker.com/tracker2.js", emptyList(), null, false) - - testee.trackerDetected(event) - testee.trackerDetected(anotherEvent) - testee.onBrokenSiteSelected() - - val command = captureCommands().lastValue - assertTrue(command is Command.BrokenSiteFeedback) - - val brokenSiteFeedback = command as Command.BrokenSiteFeedback - assertEquals("www.tracker.com", brokenSiteFeedback.blockedTrackers) - } - - @Test - fun whenOnBrokenSiteSelectedAndNoSurrogatesThenReturnSurrogatesEmptyString() { - givenOneActiveTabSelected() - - testee.onBrokenSiteSelected() - - val command = captureCommands().lastValue - assertTrue(command is Command.BrokenSiteFeedback) - - val brokenSiteFeedback = command as Command.BrokenSiteFeedback - assertEquals("", brokenSiteFeedback.surrogates) - } - - @Test - fun whenOnBrokenSiteSelectedAndSurrogatesThenReturnSurrogates() { - givenOneActiveTabSelected() - val surrogate = SurrogateResponse(true, "surrogate.com/test.js", "", "") - val anotherSurrogate = SurrogateResponse(true, "anothersurrogate.com/test.js", "", "") - - testee.surrogateDetected(surrogate) - testee.surrogateDetected(anotherSurrogate) - testee.onBrokenSiteSelected() - - val command = captureCommands().lastValue - assertTrue(command is Command.BrokenSiteFeedback) - - val brokenSiteFeedback = command as Command.BrokenSiteFeedback - assertEquals("surrogate.com,anothersurrogate.com", brokenSiteFeedback.surrogates) - } - - @Test - fun whenOnBrokenSiteSelectedAndSameHostSurrogatesThenDoNotReturnDuplicatedSurrogates() { - givenOneActiveTabSelected() - val surrogate = SurrogateResponse(true, "surrogate.com/test.js", "", "") - val anotherSurrogate = SurrogateResponse(true, "surrogate.com/test2.js", "", "") - - testee.surrogateDetected(surrogate) - testee.surrogateDetected(anotherSurrogate) - testee.onBrokenSiteSelected() - - val command = captureCommands().lastValue - assertTrue(command is Command.BrokenSiteFeedback) - - val brokenSiteFeedback = command as Command.BrokenSiteFeedback - assertEquals("surrogate.com", brokenSiteFeedback.surrogates) - } - @Test fun whenUserClickedTopCtaButtonAndCtaIsCovidCtaThenSubmitQuery() { val cta = HomeTopPanelCta.CovidCta() diff --git a/app/src/androidTest/java/com/duckduckgo/app/feedback/ui/BrokenSiteViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/feedback/ui/BrokenSiteViewModelTest.kt index d2c8cefac664..a05976d3d3c4 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/feedback/ui/BrokenSiteViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/feedback/ui/BrokenSiteViewModelTest.kt @@ -8,9 +8,7 @@ import com.duckduckgo.app.brokensite.BrokenSiteViewModel.Command import com.duckduckgo.app.brokensite.api.BrokenSiteSender import com.duckduckgo.app.brokensite.model.BrokenSite import com.duckduckgo.app.statistics.pixels.Pixel -import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify import org.junit.After import org.junit.Assert.assertEquals diff --git a/app/src/androidTest/java/com/duckduckgo/app/privacy/db/UserWhitelistDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/privacy/db/UserWhitelistDaoTest.kt new file mode 100644 index 000000000000..ffb3e1adb819 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/privacy/db/UserWhitelistDaoTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020 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.privacy.db + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.platform.app.InstrumentationRegistry +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.blockingObserve +import com.duckduckgo.app.global.db.AppDatabase +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class UserWhitelistDaoTest { + + @get:Rule + @Suppress("unused") + var instantTaskExecutorRule = InstantTaskExecutorRule() + + @ExperimentalCoroutinesApi + @get:Rule + var coroutinesTestRule = CoroutineTestRule() + + private lateinit var db: AppDatabase + private lateinit var dao: UserWhitelistDao + + @Before + fun before() { + db = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) + .allowMainThreadQueries() + .build() + dao = db.userWhitelistDao() + } + + @After + fun after() { + db.close() + } + + @Test + fun whenInitializedThenListIsEmpty() { + assertTrue(dao.all().blockingObserve()!!.isEmpty()) + } + + @Test + fun whenElementAddedThenListSizeIsOne() { + dao.insert(DOMAIN) + assertEquals(1, dao.all().blockingObserve()!!.size) + } + + @Test + fun whenElementAddedThenContainsIsTrue() { + dao.insert(DOMAIN) + assertTrue(dao.contains(DOMAIN)) + } + + @Test + fun wheElementDeletedThenContainsIsFalse() { + dao.insert(DOMAIN) + dao.delete(DOMAIN) + assertFalse(dao.contains(DOMAIN)) + } + + @Test + fun whenElementDoesNotExistThenContainsIsFalse() { + assertFalse(dao.contains(DOMAIN)) + } + + companion object { + const val DOMAIN = "www.example.com" + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModelTest.kt index 8082a47ad458..309010324c53 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModelTest.kt @@ -30,12 +30,12 @@ import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.GOOD import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.UNKNOWN +import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel.Command +import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel.Command.LaunchManageWhitelist +import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel.Command.LaunchReportBrokenSite import com.duckduckgo.app.statistics.pixels.Pixel -import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.PRIVACY_DASHBOARD_OPENED -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* +import com.nhaarman.mockitokotlin2.* import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.* @@ -60,9 +60,13 @@ class PrivacyDashboardViewModelTest { private var sitesVisitedLiveData: LiveData = mock() private var mockPixel: Pixel = mock() + private var commandObserver: Observer = mock() + private var commandCaptor: KArgumentCaptor = argumentCaptor() + private val testee: PrivacyDashboardViewModel by lazy { val model = PrivacyDashboardViewModel(mockUserWhitelistDao, networkLeaderboardDao, mockPixel, coroutineRule.testDispatcherProvider) model.viewState.observeForever(viewStateObserver) + model.command.observeForever(commandObserver) model } @@ -77,6 +81,7 @@ class PrivacyDashboardViewModelTest { @After fun after() { testee.viewState.removeObserver(viewStateObserver) + testee.command.removeObserver(commandObserver) testee.onCleared() } @@ -213,6 +218,21 @@ class PrivacyDashboardViewModelTest { assertFalse(viewState.shouldShowTrackerNetworkLeaderboard) } + @Test + fun whenManageWhitelistSelectedThenPixelIsFiredAndCommandIsManageWhitelist() { + testee.onManageWhitelistSelected() + verify(mockPixel).fire(PRIVACY_DASHBOARD_MANAGE_WHITELIST) + verify(commandObserver).onChanged(LaunchManageWhitelist) + } + + @Test + fun whenBrokenSiteSelectedThenPixelIsFiredAndCommandIsLaunchBrokenSite() { + testee.onReportBrokenSiteSelected() + verify(mockPixel).fire(PRIVACY_DASHBOARD_REPORT_BROKEN_SITE) + verify(commandObserver).onChanged(commandCaptor.capture()) + assertTrue(commandCaptor.lastValue is LaunchReportBrokenSite) + } + private fun givenSiteWithPrivacyOn() { whenever(mockUserWhitelistDao.contains(any())).thenReturn(false) testee.onSiteChanged(site()) diff --git a/app/src/androidTest/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt index 48a624d41884..1e32cab35bf5 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/settings/SettingsViewModelTest.kt @@ -207,6 +207,13 @@ class SettingsViewModelTest { assertTrue(latestViewState().showDefaultBrowserSetting) } + @Test + fun whenWhitelistSelectedThenPixelIsSentAndWhitelistLaunched() { + testee.onManageWhitelistSelected() + verify(mockPixel).fire(Pixel.PixelName.SETTINGS_MANAGE_WHITELIST) + verify(commandObserver).onChanged(Command.LaunchWhitelist) + } + @Test fun whenVariantIsEmptyThenEmptyVariantIncludedInSettings() { testee.start() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e729df9d3518..f8af4ba71e32 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -276,6 +276,10 @@ android:name="com.duckduckgo.app.icon.ui.ChangeIconActivity" android:label="@string/changeIconActivityTitle" android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity" /> + + .setPositiveButton(R.string.dialogSave) { _, _ -> userAcceptedDialog(titleInput, urlInput) } diff --git a/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteActivity.kt b/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteActivity.kt index 9fc3e2d2a2e7..05d80b4fac22 100644 --- a/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteActivity.kt @@ -120,12 +120,12 @@ class BrokenSiteActivity : DuckDuckGoActivity() { private const val UPGRADED_TO_HTTPS_EXTRA = "UPGRADED_TO_HTTPS_EXTRA" private const val SURROGATES_EXTRA = "SURROGATES_EXTRA" - fun intent(context: Context, url: String, blockedTrackers: String, surrogates: String, upgradedToHttps: Boolean): Intent { + fun intent(context: Context, data: BrokenSiteData): Intent { val intent = Intent(context, BrokenSiteActivity::class.java) - intent.putExtra(URL_EXTRA, url) - intent.putExtra(BLOCKED_TRACKERS_EXTRA, blockedTrackers) - intent.putExtra(SURROGATES_EXTRA, surrogates) - intent.putExtra(UPGRADED_TO_HTTPS_EXTRA, upgradedToHttps) + intent.putExtra(URL_EXTRA, data.url) + intent.putExtra(BLOCKED_TRACKERS_EXTRA, data.blockedTrackers) + intent.putExtra(SURROGATES_EXTRA, data.surrogates) + intent.putExtra(UPGRADED_TO_HTTPS_EXTRA, data.upgradedToHttps) return intent } } diff --git a/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteData.kt b/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteData.kt new file mode 100644 index 000000000000..d94af5b02c58 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/brokensite/BrokenSiteData.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 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.brokensite + +import android.net.Uri +import com.duckduckgo.app.global.baseHost +import com.duckduckgo.app.global.model.Site + +data class BrokenSiteData( + val url: String, + val blockedTrackers: String, + val surrogates: String, + val upgradedToHttps: Boolean +) { + + companion object { + fun fromSite(site: Site?): BrokenSiteData { + val events = site?.trackingEvents + val blockedTrackers = events?.map { Uri.parse(it.trackerUrl).host }.orEmpty().distinct().joinToString(",") + val upgradedHttps = site?.upgradedHttps ?: false + val surrogates = site?.surrogates?.map { Uri.parse(it.name).baseHost }.orEmpty().distinct().joinToString(",") + val url = site?.url.orEmpty() + return BrokenSiteData(url, blockedTrackers, surrogates, upgradedHttps) + } + } +} \ No newline at end of file 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 f02a32a0f1d3..66952a00dc04 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -56,6 +56,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment import com.duckduckgo.app.brokensite.BrokenSiteActivity +import com.duckduckgo.app.brokensite.BrokenSiteData import com.duckduckgo.app.browser.BrowserTabViewModel.* import com.duckduckgo.app.browser.autocomplete.BrowserAutoCompleteSuggestionsAdapter import com.duckduckgo.app.browser.downloader.FileDownloadNotificationManager @@ -76,6 +77,7 @@ import com.duckduckgo.app.browser.useragent.UserAgentProvider import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.global.ViewModelFactory import com.duckduckgo.app.global.device.DeviceInfo +import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.view.* import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.renderer.icon @@ -495,7 +497,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { hideKeyboard() } is Command.BrokenSiteFeedback -> { - launchBrokenSiteFeedback(it.url, it.blockedTrackers, it.surrogates, it.httpsUpgraded) + launchBrokenSiteFeedback(it.data) } is Command.ShowFullScreen -> { webViewFullScreenContainer.addView( @@ -534,10 +536,10 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope { } } - private fun launchBrokenSiteFeedback(url: String, blockedTrackers: String, surrogates: String, upgradedHttps: Boolean) { + private fun launchBrokenSiteFeedback(data: BrokenSiteData) { context?.let { val options = ActivityOptions.makeSceneTransitionAnimation(browserActivity).toBundle() - startActivity(BrokenSiteActivity.intent(it, url, blockedTrackers, surrogates, upgradedHttps), options) + startActivity(BrokenSiteActivity.intent(it, data), options) } } 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 e60167d2a5b7..3cb597b3de42 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -41,6 +41,7 @@ import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.A import com.duckduckgo.app.bookmarks.db.BookmarkEntity import com.duckduckgo.app.bookmarks.db.BookmarksDao import com.duckduckgo.app.bookmarks.ui.EditBookmarkDialogFragment.EditBookmarkListener +import com.duckduckgo.app.brokensite.BrokenSiteData import com.duckduckgo.app.browser.BrowserTabViewModel.Command.* import com.duckduckgo.app.browser.BrowserTabViewModel.GlobalLayoutViewState.Browser import com.duckduckgo.app.browser.BrowserTabViewModel.GlobalLayoutViewState.Invalidated @@ -184,7 +185,7 @@ class BrowserTabViewModel( class ShareLink(val url: String) : Command() class CopyLink(val url: String) : Command() class FindInPageCommand(val searchTerm: String) : Command() - class BrokenSiteFeedback(val url: String, val blockedTrackers: String, val surrogates: String, val httpsUpgraded: Boolean) : Command() + class BrokenSiteFeedback(val data: BrokenSiteData) : Command() object DismissFindInPage : Command() class ShowFileChooser(val filePathCallback: ValueCallback>, val fileChooserParams: WebChromeClient.FileChooserParams) : Command() class HandleExternalAppLink(val appLink: IntentType) : Command() @@ -712,13 +713,7 @@ class BrowserTabViewModel( } fun onBrokenSiteSelected() { - val events = site?.trackingEvents - val blockedTrackers = events?.map { Uri.parse(it.trackerUrl).host }.orEmpty().distinct().joinToString(",") - val upgradedHttps = site?.upgradedHttps ?: false - val surrogates = site?.surrogates?.map { Uri.parse(it.name).baseHost }.orEmpty().distinct().joinToString(",") - val url = url.orEmpty() - - command.value = BrokenSiteFeedback(url, blockedTrackers, surrogates, upgradedHttps) + command.value = BrokenSiteFeedback(BrokenSiteData.fromSite(site)) } fun onUserSelectedToEditQuery(query: String) { 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 f1c5c286cedb..1e8f4b995ffb 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 @@ -222,13 +222,13 @@ class CtaViewModel @Inject constructor( val nonNullSite = site ?: return null val host = nonNullSite.domain - if (host != null && userWhitelistDao.contains(host)) { + if (host == null || userWhitelistDao.contains(host)) { return null } nonNullSite.let { // Is major network - if (it.entity != null && host != null) { + if (it.entity != null) { it.entity?.let { entity -> if (!daxDialogNetworkShown() && DaxDialogCta.mainTrackerNetworks.contains(entity.displayName)) { return DaxDialogCta.DaxMainNetworkCta(onboardingStore, appInstallStore, entity.displayName, host) @@ -241,7 +241,7 @@ class CtaViewModel @Inject constructor( } // Trackers blocked - return if (!daxDialogTrackersFoundShown() && !isSerpUrl(it.url) && hasTrackersInformation(it.trackingEvents) && host != null) { + return if (!daxDialogTrackersFoundShown() && !isSerpUrl(it.url) && hasTrackersInformation(it.trackingEvents)) { DaxDialogCta.DaxTrackersBlockedCta(onboardingStore, appInstallStore, it.trackingEvents, host) } else if (!isSerpUrl(it.url) && !daxDialogOtherShown() && !daxDialogTrackersFoundShown() && !daxDialogNetworkShown()) { DaxDialogCta.DaxNoSerpCta(onboardingStore, appInstallStore) diff --git a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt index 4d7ac174deaf..b4b7b583f928 100644 --- a/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/AndroidBindingModule.kt @@ -38,10 +38,7 @@ import com.duckduckgo.app.launch.LaunchBridgeActivity import com.duckduckgo.app.notification.NotificationHandlerService import com.duckduckgo.app.onboarding.ui.OnboardingActivity import com.duckduckgo.app.onboarding.ui.page.DefaultBrowserPage -import com.duckduckgo.app.privacy.ui.PrivacyDashboardActivity -import com.duckduckgo.app.privacy.ui.PrivacyPracticesActivity -import com.duckduckgo.app.privacy.ui.ScorecardActivity -import com.duckduckgo.app.privacy.ui.TrackerNetworksActivity +import com.duckduckgo.app.privacy.ui.* import com.duckduckgo.app.settings.SettingsActivity import com.duckduckgo.app.survey.ui.SurveyActivity import com.duckduckgo.app.systemsearch.SystemSearchActivity @@ -92,6 +89,10 @@ abstract class AndroidBindingModule { @ContributesAndroidInjector abstract fun privacyTermsActivity(): PrivacyPracticesActivity + @ActivityScoped + @ContributesAndroidInjector + abstract fun whitelistActivity(): WhitelistActivity + @ActivityScoped @ContributesAndroidInjector abstract fun feedbackActivity(): FeedbackActivity diff --git a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt index e7dadab21a4a..b5e4fe94187f 100644 --- a/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt +++ b/app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt @@ -52,10 +52,7 @@ import com.duckduckgo.app.onboarding.ui.page.DefaultBrowserPageViewModel import com.duckduckgo.app.playstore.PlayStoreUtils import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao import com.duckduckgo.app.privacy.db.UserWhitelistDao -import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel -import com.duckduckgo.app.privacy.ui.PrivacyPracticesViewModel -import com.duckduckgo.app.privacy.ui.ScorecardViewModel -import com.duckduckgo.app.privacy.ui.TrackerNetworksViewModel +import com.duckduckgo.app.privacy.ui.* import com.duckduckgo.app.referral.AppInstallationReferrerStateListener import com.duckduckgo.app.settings.SettingsViewModel import com.duckduckgo.app.settings.db.SettingsDataStore @@ -127,6 +124,7 @@ class ViewModelFactory @Inject constructor( isAssignableFrom(ScorecardViewModel::class.java) -> ScorecardViewModel(userWhitelistDao) isAssignableFrom(TrackerNetworksViewModel::class.java) -> TrackerNetworksViewModel() isAssignableFrom(PrivacyPracticesViewModel::class.java) -> PrivacyPracticesViewModel() + isAssignableFrom(WhitelistViewModel::class.java) -> WhitelistViewModel(userWhitelistDao) isAssignableFrom(FeedbackViewModel::class.java) -> FeedbackViewModel(playStoreUtils, feedbackSubmitter) isAssignableFrom(BrokenSiteViewModel::class.java) -> BrokenSiteViewModel(pixel, brokenSiteSender) isAssignableFrom(SurveyViewModel::class.java) -> SurveyViewModel(surveyDao, statisticsStore, appInstallStore) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/db/UserWhitelistDao.kt b/app/src/main/java/com/duckduckgo/app/privacy/db/UserWhitelistDao.kt index ceae950f9db2..2ec6d9ae047e 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/db/UserWhitelistDao.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/db/UserWhitelistDao.kt @@ -16,7 +16,7 @@ package com.duckduckgo.app.privacy.db -import androidx.core.net.toUri +import androidx.lifecycle.LiveData import androidx.room.* import com.duckduckgo.app.privacy.model.UserWhitelistedDomain @@ -37,6 +37,9 @@ abstract class UserWhitelistDao { delete(UserWhitelistedDomain(domain)) } + @Query("select * from user_whitelist") + abstract fun all(): LiveData> + @Query("select count(1) > 0 from user_whitelist where domain = :domain") abstract fun contains(domain: String): Boolean } diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt index 8b1b03be090b..a289fd403cad 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt @@ -23,6 +23,8 @@ import android.os.Bundle import android.view.View import androidx.core.content.ContextCompat import androidx.lifecycle.Observer +import com.duckduckgo.app.brokensite.BrokenSiteActivity +import com.duckduckgo.app.brokensite.BrokenSiteData import com.duckduckgo.app.browser.R import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.model.Site @@ -30,6 +32,9 @@ import com.duckduckgo.app.global.view.hide import com.duckduckgo.app.global.view.html import com.duckduckgo.app.global.view.show import com.duckduckgo.app.privacy.renderer.* +import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel.Command +import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel.Command.LaunchManageWhitelist +import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel.Command.LaunchReportBrokenSite import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel.ViewState import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* @@ -56,29 +61,45 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_privacy_dashboard) configureToolbar() + setupObservers() + setupClickListeners() + } + private fun configureToolbar() { + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + private fun setupObservers() { viewModel.viewState.observe(this, Observer { it?.let { render(it) } }) - + viewModel.command.observe(this, Observer { + it?.let { processCommand(it) } + }) repository.retrieveSiteData(intent.tabId!!).observe(this, Observer { viewModel.onSiteChanged(it) }) + } + private fun setupClickListeners() { privacyGrade.setOnClickListener { onScorecardClicked() } + whitelistButton.setOnClickListener { + viewModel.onManageWhitelistSelected() + } + + brokenSiteButton.setOnClickListener { + viewModel.onReportBrokenSiteSelected() + } + privacyToggle.setOnCheckedChangeListener { _, enabled -> viewModel.onPrivacyToggled(enabled) } } - private fun configureToolbar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun render(viewState: ViewState) { if (isFinishing) { return @@ -132,11 +153,26 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { privacyToggle.isChecked = enabled } + private fun processCommand(command: Command) { + when (command) { + is LaunchManageWhitelist -> launchWhitelistActivity() + is LaunchReportBrokenSite -> launchReportBrokenSite(command.data) + } + } + private fun onScorecardClicked() { pixel.fire(PRIVACY_DASHBOARD_SCORECARD) startActivity(ScorecardActivity.intent(this, intent.tabId!!)) } + private fun launchReportBrokenSite(data: BrokenSiteData) { + startActivity(BrokenSiteActivity.intent(this, data)) + } + + private fun launchWhitelistActivity() { + startActivity(WhitelistActivity.intent(this)) + } + fun onEncryptionClicked(@Suppress("UNUSED_PARAMETER") view: View) { pixel.fire(PRIVACY_DASHBOARD_ENCRYPTION) } @@ -174,5 +210,4 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { } } - } diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt index 5718aa2a7be0..b8e88881c8e6 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt @@ -21,8 +21,10 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel +import com.duckduckgo.app.brokensite.BrokenSiteData import com.duckduckgo.app.global.DefaultDispatcherProvider import com.duckduckgo.app.global.DispatcherProvider +import com.duckduckgo.app.global.SingleLiveEvent import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao @@ -32,6 +34,8 @@ import com.duckduckgo.app.privacy.model.HttpsStatus import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.UNKNOWN +import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel.Command.LaunchManageWhitelist +import com.duckduckgo.app.privacy.ui.PrivacyDashboardViewModel.Command.LaunchReportBrokenSite import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* import kotlinx.coroutines.GlobalScope @@ -59,7 +63,13 @@ class PrivacyDashboardViewModel( val shouldReloadPage: Boolean ) + sealed class Command { + object LaunchManageWhitelist : Command() + class LaunchReportBrokenSite(val data: BrokenSiteData) : Command() + } + val viewState: MutableLiveData = MutableLiveData() + val command: SingleLiveEvent = SingleLiveEvent() private var site: Site? = null private val sitesVisited: LiveData = networkLeaderboardDao.sitesVisited() @@ -168,6 +178,16 @@ class PrivacyDashboardViewModel( } } + fun onManageWhitelistSelected() { + pixel.fire(PRIVACY_DASHBOARD_MANAGE_WHITELIST) + command.value = LaunchManageWhitelist + } + + fun onReportBrokenSiteSelected() { + pixel.fire(PRIVACY_DASHBOARD_REPORT_BROKEN_SITE) + command.value = LaunchReportBrokenSite(BrokenSiteData.fromSite(site)) + } + private companion object { private const val LEADERBOARD_MIN_NETWORKS = 3 private const val LEADERBOARD_MIN_DOMAINS_EXCLUSIVE = 30 diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt new file mode 100644 index 000000000000..97451ae07864 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2020 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.privacy.ui + +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.* +import android.widget.ImageView +import android.widget.PopupMenu +import android.widget.TextView +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter +import androidx.recyclerview.widget.RecyclerView.VERTICAL +import com.duckduckgo.app.browser.R +import com.duckduckgo.app.global.DuckDuckGoActivity +import com.duckduckgo.app.global.view.gone +import com.duckduckgo.app.global.view.html +import com.duckduckgo.app.global.view.show +import com.duckduckgo.app.privacy.model.UserWhitelistedDomain +import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.ConfirmDelete +import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.ShowEdit +import kotlinx.android.synthetic.main.activity_whitelist.* +import kotlinx.android.synthetic.main.edit_whitelist.* +import kotlinx.android.synthetic.main.include_toolbar.* +import kotlinx.android.synthetic.main.view_whitelist_entry.view.* + +class WhitelistActivity : DuckDuckGoActivity() { + + lateinit var adapter: WhitelistAdapter + + private var dialog: AlertDialog? = null + private val viewModel: WhitelistViewModel by bindViewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_whitelist) + setupActionBar() + setupRecycler() + observeViewModel() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.whitelist_activity_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.add -> viewModel.onAddRequested() + } + return super.onOptionsItemSelected(item) + } + + private fun setupRecycler() { + adapter = WhitelistAdapter(viewModel) + recycler.adapter = adapter + + val separator = DividerItemDecoration(this, VERTICAL) + recycler.addItemDecoration(separator) + } + + private fun setupActionBar() { + setSupportActionBar(toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + private fun observeViewModel() { + viewModel.viewState.observe(this, Observer { viewState -> + viewState?.let { renderViewState(it) } + }) + + viewModel.command.observe(this, Observer { + processCommand(it) + }) + } + + private fun renderViewState(viewState: WhitelistViewModel.ViewState) { + adapter.entries = viewState.whitelist + if (viewState.showWhitelist) { + showList() + invalidateOptionsMenu() + } else { + hideList() + } + } + + private fun showList() { + recycler.show() + emptyWhitelist.gone() + } + + private fun hideList() { + recycler.gone() + emptyWhitelist.show() + } + + private fun processCommand(command: WhitelistViewModel.Command?) { + when (command) { + is WhitelistViewModel.Command.ShowAdd -> showAddDialog() + is ShowEdit -> showEditDialog(command.entry) + is ConfirmDelete -> showDeleteDialog(command.entry) + } + } + + private fun showAddDialog() { + val addDialog = AlertDialog.Builder(this).apply { + setTitle(R.string.dialogAddTitle) + setView(R.layout.edit_whitelist) + setPositiveButton(android.R.string.yes) { _, _ -> + val newText = dialog?.textInput?.text.toString() + viewModel.onEntryAdded(UserWhitelistedDomain(newText)) + } + setNegativeButton(android.R.string.no) { _, _ -> } + }.create() + + dialog?.dismiss() + dialog = addDialog + addDialog.show() + } + + private fun showEditDialog(entry: UserWhitelistedDomain) { + val editDialog = AlertDialog.Builder(this).apply { + setTitle(R.string.dialogEditTitle) + textInput.setText(entry.domain) + setView(R.layout.edit_whitelist) + setPositiveButton(android.R.string.yes) { _, _ -> + val newText = textInput.text.toString() + viewModel.onEntryEdited(entry, UserWhitelistedDomain(newText)) + } + setNegativeButton(android.R.string.no) { _, _ -> } + }.create() + + dialog?.dismiss() + dialog = editDialog + editDialog.show() + } + + private fun showDeleteDialog(entry: UserWhitelistedDomain) { + val deleteDialog = AlertDialog.Builder(this).apply { + setTitle(R.string.dialogConfirmTitle) + setMessage(getString(R.string.whitelistEntryDeleteConfirmMessage, entry.domain).html(this.context)) + setPositiveButton(android.R.string.yes) { _, _ -> viewModel.onEntryDeleted(entry) } + setNegativeButton(android.R.string.no) { _, _ -> } + }.create() + + dialog?.dismiss() + dialog = deleteDialog + deleteDialog.show() + } + + override fun onDestroy() { + dialog?.dismiss() + super.onDestroy() + } + + companion object { + fun intent(context: Context): Intent { + return Intent(context, WhitelistActivity::class.java) + } + } + + class WhitelistAdapter(private val viewModel: WhitelistViewModel) : Adapter() { + + var entries: List = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WhitelistViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.view_whitelist_entry, parent, false) + return WhitelistViewHolder(view, view.domain, view.overflowMenu) + } + + override fun onBindViewHolder(holder: WhitelistViewHolder, position: Int) { + val entry = entries[position] + holder.domain.text = entry.domain + holder.root.setOnClickListener { + viewModel.onEditRequested(entry) + } + + val overflowContentDescription = holder.overflowMenu.context.getString(R.string.whitelistEntryOverflowContentDescription, entry.domain) + holder.overflowMenu.contentDescription = overflowContentDescription + holder.overflowMenu.setOnClickListener { + showOverflowMenu(holder.overflowMenu, entry) + } + } + + private fun showOverflowMenu(overflowMenu: ImageView, entry: UserWhitelistedDomain) { + val popup = PopupMenu(overflowMenu.context, overflowMenu) + popup.inflate(R.menu.whitelist_individual_overflow_menu) + popup.setOnMenuItemClickListener { + when (it.itemId) { + R.id.edit -> { + viewModel.onEditRequested(entry) + true + } + R.id.delete -> { + viewModel.onDeleteRequested(entry) + true + } + else -> false + } + } + popup.show() + } + + override fun getItemCount(): Int { + return entries.size + } + } + + class WhitelistViewHolder( + val root: View, + val domain: TextView, + val overflowMenu: ImageView + ) : RecyclerView.ViewHolder(root) +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt new file mode 100644 index 000000000000..28589c65540b --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020 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.privacy.ui + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel +import com.duckduckgo.app.global.DefaultDispatcherProvider +import com.duckduckgo.app.global.DispatcherProvider +import com.duckduckgo.app.global.SingleLiveEvent +import com.duckduckgo.app.privacy.db.UserWhitelistDao +import com.duckduckgo.app.privacy.model.UserWhitelistedDomain +import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class WhitelistViewModel( + private val dao: UserWhitelistDao, + private val dispatchers: DispatcherProvider = DefaultDispatcherProvider() + +) : ViewModel() { + + data class ViewState( + val showWhitelist: Boolean = true, + val whitelist: List = emptyList() + ) + + sealed class Command { + object ShowAdd : Command() + class ShowEdit(val entry: UserWhitelistedDomain) : Command() + class ConfirmDelete(val entry: UserWhitelistedDomain) : Command() + } + + val viewState: MutableLiveData = MutableLiveData() + val command: SingleLiveEvent = SingleLiveEvent() + + private val entries: LiveData> = dao.all() + private val observer = Observer> { onUserWhitelistChanged(it!!) } + + init { + viewState.value = ViewState() + entries.observeForever(observer) + } + + override fun onCleared() { + super.onCleared() + entries.removeObserver(observer) + } + + private fun onUserWhitelistChanged(entries: List) { + viewState.value = viewState.value?.copy( + showWhitelist = entries.isNotEmpty(), + whitelist = entries + ) + } + + fun onAddRequested() { + command.value = ShowAdd + } + + fun onEntryAdded(entry: UserWhitelistedDomain) { + GlobalScope.launch(dispatchers.io()) { + dao.insert(entry) + } + } + + fun onEditRequested(entry: UserWhitelistedDomain) { + command.value = ShowEdit(entry) + } + + fun onEntryEdited(old: UserWhitelistedDomain, new: UserWhitelistedDomain) { + onEntryDeleted(old) + onEntryAdded(new) + } + + fun onDeleteRequested(entry: UserWhitelistedDomain) { + command.value = ConfirmDelete(entry) + } + + fun onEntryDeleted(entry: UserWhitelistedDomain) { + GlobalScope.launch(dispatchers.io()) { + dao.delete(entry) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt index 076907a68b51..3a48ccbbb273 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsActivity.kt @@ -35,35 +35,19 @@ import com.duckduckgo.app.global.DuckDuckGoActivity import com.duckduckgo.app.global.sendThemeChangedBroadcast import com.duckduckgo.app.global.view.gone import com.duckduckgo.app.global.view.launchDefaultAppActivity -import com.duckduckgo.app.icon.ui.ChangeIconActivity import com.duckduckgo.app.global.view.show +import com.duckduckgo.app.icon.ui.ChangeIconActivity +import com.duckduckgo.app.privacy.ui.WhitelistActivity import com.duckduckgo.app.settings.SettingsViewModel.AutomaticallyClearData import com.duckduckgo.app.settings.SettingsViewModel.Command import com.duckduckgo.app.settings.clear.ClearWhatOption import com.duckduckgo.app.settings.clear.ClearWhenOption import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName -import kotlinx.android.synthetic.main.content_settings_general.autocompleteToggle -import kotlinx.android.synthetic.main.content_settings_general.changeAppIcon -import kotlinx.android.synthetic.main.content_settings_general.changeAppIconLabel -import kotlinx.android.synthetic.main.content_settings_general.lightThemeToggle -import kotlinx.android.synthetic.main.content_settings_general.setAsDefaultBrowserSetting -import kotlinx.android.synthetic.main.content_settings_other.about -import kotlinx.android.synthetic.main.content_settings_other.provideFeedback -import kotlinx.android.synthetic.main.content_settings_other.version -import kotlinx.android.synthetic.main.content_settings_privacy.automaticallyClearWhatSetting -import kotlinx.android.synthetic.main.content_settings_privacy.automaticallyClearWhenSetting -import kotlinx.android.synthetic.main.include_toolbar.toolbar -import kotlinx.android.synthetic.main.content_settings_general.autocompleteToggle -import kotlinx.android.synthetic.main.content_settings_general.lightThemeToggle -import kotlinx.android.synthetic.main.content_settings_general.searchNotificationToggle -import kotlinx.android.synthetic.main.content_settings_general.setAsDefaultBrowserSetting -import kotlinx.android.synthetic.main.content_settings_other.about -import kotlinx.android.synthetic.main.content_settings_other.provideFeedback -import kotlinx.android.synthetic.main.content_settings_other.version -import kotlinx.android.synthetic.main.content_settings_privacy.automaticallyClearWhatSetting -import kotlinx.android.synthetic.main.content_settings_privacy.automaticallyClearWhenSetting -import kotlinx.android.synthetic.main.include_toolbar.toolbar +import kotlinx.android.synthetic.main.content_settings_general.* +import kotlinx.android.synthetic.main.content_settings_other.* +import kotlinx.android.synthetic.main.content_settings_privacy.* +import kotlinx.android.synthetic.main.include_toolbar.* import javax.inject.Inject class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFragment.Listener, SettingsAutomaticallyClearWhenFragment.Listener { @@ -111,6 +95,7 @@ class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFra setAsDefaultBrowserSetting.setOnCheckedChangeListener(defaultBrowserChangeListener) automaticallyClearWhatSetting.setOnClickListener { launchAutomaticallyClearWhatDialog() } automaticallyClearWhenSetting.setOnClickListener { launchAutomaticallyClearWhenDialog() } + whitelist.setOnClickListener { viewModel.onManageWhitelistSelected() } } private fun observeViewModel() { @@ -166,6 +151,7 @@ class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFra private fun processCommand(it: Command?) { when (it) { is Command.LaunchFeedback -> launchFeedback() + is Command.LaunchWhitelist -> launchWhitelist() is Command.LaunchAppIcon -> launchAppIconChange() is Command.UpdateTheme -> sendThemeChangedBroadcast() } @@ -198,6 +184,11 @@ class SettingsActivity : DuckDuckGoActivity(), SettingsAutomaticallyClearWhatFra startActivityForResult(Intent(FeedbackActivity.intent(this)), FEEDBACK_REQUEST_CODE, options) } + private fun launchWhitelist() { + val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle() + startActivity(WhitelistActivity.intent(this), options) + } + private fun launchAppIconChange() { val options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle() startActivityForResult(Intent(ChangeIconActivity.intent(this)), CHANGE_APP_ICON_REQUEST_CODE, options) diff --git a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt index 5b94bb3fea72..e5e097c4882a 100644 --- a/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/settings/SettingsViewModel.kt @@ -65,6 +65,7 @@ class SettingsViewModel @Inject constructor( sealed class Command { object LaunchFeedback : Command() + object LaunchWhitelist: Command() object LaunchAppIcon : Command() object UpdateTheme : Command() } @@ -186,6 +187,11 @@ class SettingsViewModel @Inject constructor( ) } + fun onManageWhitelistSelected() { + pixel.fire(SETTINGS_MANAGE_WHITELIST) + command.value = Command.LaunchWhitelist + } + private fun currentViewState(): ViewState { return viewState.value!! } diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index ce9faf8ce0a3..a4dbdab2339a 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -72,7 +72,9 @@ interface Pixel { PRIVACY_DASHBOARD_NETWORKS("mp_n"), PRIVACY_DASHBOARD_WHITELIST_ADD("mp_wla"), PRIVACY_DASHBOARD_WHITELIST_REMOVE("mp_wlr"), - + PRIVACY_DASHBOARD_MANAGE_WHITELIST("mp_mw"), + PRIVACY_DASHBOARD_REPORT_BROKEN_SITE("mp_rb"), + HTTPS_NO_LOOKUP("m_https_nl"), HTTPS_LOCAL_UPGRADE("m_https_lu"), HTTPS_SERVICE_REQUEST_UPGRADE("m_https_sru"), @@ -116,6 +118,7 @@ interface Pixel { SETTINGS_OPENED("ms"), SETTINGS_THEME_TOGGLED_LIGHT("ms_tl"), SETTINGS_THEME_TOGGLED_DARK("ms_td"), + SETTINGS_MANAGE_WHITELIST("ms_mw"), SURVEY_CTA_SHOWN(pixelName = "mus_cs"), SURVEY_CTA_DISMISSED(pixelName = "mus_cd"), diff --git a/app/src/main/res/drawable/ic_overflow_bookmarks_24dp.xml b/app/src/main/res/drawable/ic_overflow_recycler_entries_24dp.xml similarity index 100% rename from app/src/main/res/drawable/ic_overflow_bookmarks_24dp.xml rename to app/src/main/res/drawable/ic_overflow_recycler_entries_24dp.xml diff --git a/app/src/main/res/layout/activity_whitelist.xml b/app/src/main/res/layout/activity_whitelist.xml new file mode 100644 index 000000000000..91b131f77bf9 --- /dev/null +++ b/app/src/main/res/layout/activity_whitelist.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/content_privacy_dashboard.xml b/app/src/main/res/layout/content_privacy_dashboard.xml index c245c7de2681..a96974485952 100644 --- a/app/src/main/res/layout/content_privacy_dashboard.xml +++ b/app/src/main/res/layout/content_privacy_dashboard.xml @@ -47,8 +47,8 @@ android:id="@+id/httpsContainer" android:layout_width="match_parent" android:layout_height="wrap_content" - app:layout_constraintBottom_toTopOf="@id/networksContainer" - android:onClick="onEncryptionClicked"> + android:onClick="onEncryptionClicked" + app:layout_constraintBottom_toTopOf="@id/networksContainer"> - + app:layout_constraintTop_toBottomOf="@id/privacyToggleContainer"> + android:layout_height="match_parent" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:background="@color/grayish" /> - + + + \ No newline at end of file diff --git a/app/src/main/res/layout/edit_whitelist.xml b/app/src/main/res/layout/edit_whitelist.xml new file mode 100644 index 000000000000..1681f9718ba1 --- /dev/null +++ b/app/src/main/res/layout/edit_whitelist.xml @@ -0,0 +1,37 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_bookmark_entry.xml b/app/src/main/res/layout/view_bookmark_entry.xml index 3cf3f6572c79..aa66743ad684 100644 --- a/app/src/main/res/layout/view_bookmark_entry.xml +++ b/app/src/main/res/layout/view_bookmark_entry.xml @@ -78,7 +78,7 @@ android:paddingStart="14dp" android:paddingEnd="14dp" android:scaleType="center" - android:src="@drawable/ic_overflow_bookmarks_24dp" + android:src="@drawable/ic_overflow_recycler_entries_24dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/view_whitelist_entry.xml b/app/src/main/res/layout/view_whitelist_entry.xml new file mode 100644 index 000000000000..542cfac85820 --- /dev/null +++ b/app/src/main/res/layout/view_whitelist_entry.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/whitelist_activity_menu.xml b/app/src/main/res/menu/whitelist_activity_menu.xml new file mode 100644 index 000000000000..c3809420cda1 --- /dev/null +++ b/app/src/main/res/menu/whitelist_activity_menu.xml @@ -0,0 +1,28 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/whitelist_individual_overflow_menu.xml b/app/src/main/res/menu/whitelist_individual_overflow_menu.xml new file mode 100644 index 000000000000..d66fde37780b --- /dev/null +++ b/app/src/main/res/menu/whitelist_individual_overflow_menu.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 42100a9ac341..2b01fcb961e2 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -188,11 +188,11 @@ Отметки Отметки Наистина ли искате да изтриете отметка <b>%s</b>? - Потвърдете + Потвърдете Добавена е отметка Заглавие на отметката URL адрес на отметката - Запазване + Запазване Редактиране на отметка Все още няма добавени отметки Още опции за отметка %s diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 85e6b9b257d4..905674c6092a 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -192,11 +192,11 @@ Záložky Záložky Opravdu chcete smazat záložku <b>%s</b>? - Potvrdit + Potvrdit Záložka přidána Název záložky URL záložky - Uložit + Uložit Upravit záložku Zatím nebyly přidány žádné záložky. Další možnosti záložky %s diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index d1a6be46a338..26b59a139132 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -188,11 +188,11 @@ Bogmærker Bogmærker Er du sikker på, at du vil slette bogmærket <b>%s</b>? - Bekræft + Bekræft Bogmærke tilføjet Titel på bogmærke Bogmærkets URL - Gem + Gem Rediger bogmærke Ingen bogmærker tilføjet endnu Flere muligheder for bogmærke %s diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3dc74ceb4418..a8948a2cde00 100755 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -188,11 +188,11 @@ Lesezeichen Lesezeichen Bist du sicher, dass du das Lesezeichen <b>%s</b> löschen möchtest? - Bestätigen + Bestätigen Lesezeichen hinzugefügt Lesezeichen-Titel Lesezeichen-URL - Speichern + Speichern Lesezeichen bearbeiten Noch keine Lesezeichen hinzugefügt Mehr Optionen für Lesezeichen %s diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 64734374589a..a1f4117cb105 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -188,11 +188,11 @@ Σελιδοδείκτες Σελιδοδείκτες Θέλετε σίγουρα να διαγράψετε τον σελιδοδείκτη <b>%s</b>; - Επιβεβαίωση + Επιβεβαίωση Ο σελιδοδείκτης προστέθηκε Τίτλος σελιδοδείκτη Διεύθυνση URL σελιδοδείκτη - Αποθήκευση + Αποθήκευση Επεξεργασία σελιδοδείκτη Δεν προστέθηκαν σελιδοδείκτες ακόμα Περισσότερες επιλογές για σελιδοδείκτη %s diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2d631e319f8a..7ea661d2b5b6 100755 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -188,11 +188,11 @@ Marcadores Marcadores ¿Estás seguro de que quieres eliminar el marcador <b>%s</b>? - Confirmar + Confirmar Marcador añadido Título del marcador URL del marcador - Guardar + Guardar Editar marcador No se han añadido marcadores todavía Más opciones para el marcador %s diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 5ca2916616fe..87fafd1e7704 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -206,11 +206,11 @@ Järjehoidjad Järjehoidjad Kas soovite kindlasti kustutada järjehoidja <b>%s</b>? - Kinnitage + Kinnitage Järjehoidja lisatud Järjehoidja pealkiri Järjehoidja URL - Salvesta + Salvesta Redigeeri järjehoidjat Järjehoidjaid pole veel lisatud Veel järjehoidja %s valikuid diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 8ce495eeb530..334ce867c1fe 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -188,11 +188,11 @@ Kirjanmerkit Kirjanmerkit Haluatko varmasti poistaa kirjanmerkin <b>%s</b>? - Vahvista + Vahvista Kirjanmerkki lisätty Kirjanmerkin otsikko Kirjanmerkin URL-osoite - Tallenna + Tallenna Muokkaa kirjanmerkkiä Kirjanmerkkejä ei ole vielä lisätty Lisää vaihtoehtoja kirjanmerkille %s diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1bd9b11f1bde..137287c801a1 100755 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -188,11 +188,11 @@ Signets Signets Êtes-vous sûr(e) de vouloir supprimer le signet <b>%s</b> ? - Confirmer + Confirmer Signet ajouté Titre du signet URL du signet - Enregistrer + Enregistrer Modifier le signet Aucun signet ajouté pour le moment Plus d\'options pour le signet %s diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 7eed226b1a2e..7d5151e2091a 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -188,11 +188,11 @@ Knjižne oznake Knjižne oznake Sigurno želite izbrisati knjižnu oznaku <b>%s</b>? - Potvrdi + Potvrdi Knjižna oznaka je dodana Naslov knjižne oznake URL knjižne oznake - Spremi + Spremi Uredi knjižnu oznaku Još nema dodanih knjižnih oznaka Više opcija za knjižnu oznaku %s diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 22e1c300df84..179af0851f6f 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -188,11 +188,11 @@ Könyvjelzők Könyvjelzők Biztos, hogy törlöd a(z) <b>%s</b> könyvjelzőt? - Megerősítés + Megerősítés Könyvjelző hozzáadva Könyvjelző címe Könyvjelző URL - Mentés + Mentés Könyvjelző szerkesztése Még nincsenek könyvjelzők hozzáadva %s könyvjelző további lehetőségei diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b6028fe93496..22dd21e7622e 100755 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -204,11 +204,11 @@ Segnalibri Segnalibri Sei sicuro di voler eliminare il segnalibro <b>%s</b>? - Conferma + Conferma Segnalibro aggiunto Titolo del segnalibro URL del segnalibro - Salva + Salva Modifica segnalibro Non è ancora stato aggiunto nessun segnalibro Ulteriori opzioni per il segnalibro %s diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index ae42823acf6b..2a7ce3bc63f0 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -192,11 +192,11 @@ Žymelės Žymelės Ar tikrai norite ištrinti žymelę <b>%s</b>? - Patvirtinti + Patvirtinti Žymelė pridėta Žymelės antraštė Žymelės URL - Išsaugoti + Išsaugoti Redaguoti žymelę Žymelių dar nepridėta Daugiau parinkčių žymelei %s diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index e4dbb07ccca6..78d1db5b3a22 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -210,11 +210,11 @@ Grāmatzīmes Grāmatzīmes Vai tiešām vēlaties izdzēst grāmatzīmi <b>%s</b>? - Apstiprināt + Apstiprināt Pievienota grāmatzīme Grāmatzīmes nosaukums Grāmatzīmju URL - Saglabāt + Saglabāt Rediģēt grāmatzīmi Pagaidām nav pievienota neviena grāmatzīme Vairāk iespēju grāmatzīmei %s diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 9ab7e777ccda..25992c4f62b7 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -188,11 +188,11 @@ Bokmerker Bokmerker Er du sikker på at du vil slette bokmerket <b>%s</b>? - Bekreft + Bekreft Bokmerke lagt til Bokmerketittel Bokmerke-URL - Lagre + Lagre Rediger bokmerke Ingen bokmerker lagt til ennå Flere alternativer for bokmerket %s diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0bd4c88a1b9c..a2765b256fc1 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -188,11 +188,11 @@ Bladwijzers Bladwijzers Weet je zeker dat je bladwijzer <b>%s</b> wilt verwijderen? - Bevestigen + Bevestigen Bladwijzer toegevoegd Naam bladwijzer URL bladwijzer - Opslaan + Opslaan Bladwijzer bewerken Nog geen bladwijzers toegevoegd Meer opties voor bladwijzer %s diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a34e1e19e624..e2fb2d3d8af6 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -196,11 +196,11 @@ Zakładki Zakładki Czy na pewno chcesz usunąć zakładkę <b>%s</b>? - Potwierdź + Potwierdź Zakładka została dodana Tytuł zakładki Adres URL zakładki - Zapisz + Zapisz Edytuj zakładkę Nie dodano jeszcze żadnych zakładek Więcej opcji dla zakładki %s diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 372776f951c9..21fae677c05b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -188,11 +188,11 @@ Marcadores Marcadores Tem a certeza de que pretende eliminar o marcador <b>%s</b>? - Confirmar + Confirmar Marcador adicionado Título do marcador URL do marcador - Guardar + Guardar Editar marcador Ainda não foram adicionados marcadores Mais opções para o marcador %s diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 8bb06335306a..f2760015c89c 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -192,11 +192,11 @@ Semne de carte Semne de carte Sunteți sigur că doriți să ștergeți semnul de carte <b>%s</b>? - Confirmare + Confirmare Semn de carte adăugat Titlu semn de carte Adresă URL semn de carte - Salvare + Salvare Editare semn de carte Nu s-a adăugat niciun semn de carte Mai multe opțiuni pentru semnul de carte %s diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 700bb07a4d40..50289d21bb81 100755 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -211,11 +211,11 @@ Закладки Закладки Вы действительно хотите удалить закладку <b>%s</b>? - Подтвердить + Подтвердить Закладка добавлена Название закладки URL закладки - Сохранить + Сохранить Редактировать закладку Закладок пока нет Другие параметры управления закладкой %s diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index d87c93e2b81e..7318a4925378 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -189,11 +189,11 @@ Záložky Záložky Naozaj chcete odstrániť záložku <b>%s</b>? - Potvrdiť + Potvrdiť Záložka je pridaná Názov záložky Webová adresa záložky - Uložiť + Uložiť Upraviť záložku Zatiaľ neboli pridané žiadne záložky Viac možností pre záložku %s diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index f344e2bbcd97..a8fa8ab65aa0 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -196,11 +196,11 @@ Zaznamki Zaznamki Ste prepričani, da želite izbrisati zaznamek <b>%s</b>? - Potrdi + Potrdi Zaznamek je dodan Naslov zaznamka URL zaznamka - Shrani + Shrani Uredi zaznamek Zaznamki še niso dodani Več možnosti za zaznamek %s diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index be541c33c050..e26027879019 100755 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -204,11 +204,11 @@ Bokmärken Bokmärken Är du säker på att du vill radera bokmärket <b>%s</b>? - Bekräfta + Bekräfta Bokmärket har lagts till Bokmärkesrubrik Bokmärkets URL - Spara + Spara Redigera bokmärke Inga bokmärken har lagts till ännu Fler alternativ för bokmärke %s diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index c3907817a132..28e71f64c933 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -209,11 +209,11 @@ Yer imleri Yer imleri <b>%s</b> yer imini silmek istediğinizden emin misiniz? - Onayla + Onayla Yer imi eklendi Yer imi başlığı Yer imi URL\'si - Kaydet + Kaydet Yer İşaretini Düzenle Henüz yer işareti eklenmedi %s yer imi için diğer seçenekler diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index cdcff0dce9e8..aab05120d893 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -31,6 +31,7 @@ + diff --git a/app/src/main/res/values/string-untranslated.xml b/app/src/main/res/values/string-untranslated.xml index 8ce4d79ac97c..77841796b711 100644 --- a/app/src/main/res/values/string-untranslated.xml +++ b/app/src/main/res/values/string-untranslated.xml @@ -33,10 +33,29 @@ Remove Search - + Site Privacy Protection + SITE PROTECTION ENABLED + SITE PROTECTION DISABLED Manage Whitelist Report Broken Site + + + Add + Edit + + + Whitelist + More options for whitelist entry %s + Add to Whitelist + Are you sure you want to delete <b>%s</b> from whitelist? + These whitelisted sites will not be upgraded by Privacy Protection + No whitelisted sites yet + www.example.com + + + Privacy Protection Whitelist + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 185c95477f7e..10e3b99161c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,8 +77,6 @@ Privacy Dashboard - PRIVACY PROTECTION ENABLED - PRIVACY PROTECTION DISABLED ENHANCED FROM <img src="%d" /> TO <img src="%d" /> Privacy Grade @@ -194,17 +192,19 @@ Bookmarks Bookmarks Are you sure you want to delete bookmark <b>%s</b>? - Confirm Bookmark added Bookmark title Bookmark URL - Save Edit Bookmark No bookmarks added yet More options for bookmark %s + Bookmark added + + + Confirm + Save Delete Edit - Bookmark added Search DuckDuckGo diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index b4290a7d1f16..74fd414a93ae 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -50,6 +50,7 @@ @color/skyBlue @color/white @color/almostBlack + @color/white @color/white @color/white @color/grayishTwo @@ -118,6 +119,7 @@ @color/cornflowerBlue @color/warmerGray @color/white + @color/almostBlack @color/grayishBrown @color/almostBlack @color/warmerGray From e374aef3ae25acf7d33059646046473f36dc3042 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 1 May 2020 03:37:04 +0100 Subject: [PATCH 05/15] Add db migration test --- .../com/duckduckgo/app/global/db/AppDatabaseTest.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt index 503bf10f2999..025b634dbc39 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt @@ -185,14 +185,14 @@ class AppDatabaseTest { } @Test - fun whenMigratingFromVersion17To18IfUserDidNotSawOnboardingThenMigrateToNew() = coroutineRule.runBlocking { + fun whenMigratingFromVersion17To18IfUserDidNotSeeOnboardingThenMigrateToNew() = coroutineRule.runBlocking { givenUserNeverSawOnboarding() createDatabaseAndMigrate(17, 18, migrationsProvider.MIGRATION_17_TO_18) assertEquals(AppStage.NEW, database().userStageDao().currentUserAppStage()?.appStage) } @Test - fun whenMigratingFromVersion17To18IfUserSawOnboardingThenMigrateToEstablished() = coroutineRule.runBlocking { + fun whenMigratingFromVersion17To18IfUserSeeOnboardingThenMigrateToEstablished() = coroutineRule.runBlocking { givenUserSawOnboarding() createDatabaseAndMigrate(17, 18, migrationsProvider.MIGRATION_17_TO_18) assertEquals(AppStage.ESTABLISHED, database().userStageDao().currentUserAppStage()?.appStage) @@ -205,6 +205,12 @@ class AppDatabaseTest { assertEquals(0, database().uncaughtExceptionDao().count()) } + @Test + fun whenMigratingFromVersion19To20ThenValidationSucceeds() { + createDatabaseAndMigrate(19, 20, migrationsProvider.MIGRATION_19_TO_20) + assertEquals(0, database().uncaughtExceptionDao().count()) + } + private fun createDatabase(version: Int) { testHelper.createDatabase(TEST_DB_NAME, version).close() } From 0ae138a65d90ee83b839dcc7d2ef2793d1889970 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 4 May 2020 19:08:19 +0100 Subject: [PATCH 06/15] Add browser menu item, error messages and more tests --- .../app/brokensite/BrokenSiteDataTest.kt | 102 ++++++++++-- .../app/browser/BrowserTabViewModelTest.kt | 34 +++- .../app/privacy/ui/WhitelistViewModelTest.kt | 154 ++++++++++++++++++ .../app/browser/BrowserTabFragment.kt | 4 + .../app/browser/BrowserTabViewModel.kt | 46 +++++- .../com/duckduckgo/app/global/UriString.kt | 14 +- .../app/privacy/ui/WhitelistActivity.kt | 28 ++-- .../app/privacy/ui/WhitelistViewModel.kt | 11 +- .../duckduckgo/app/statistics/pixels/Pixel.kt | 5 +- .../popup_window_browser_bottom_tab_menu.xml | 5 + .../res/layout/popup_window_browser_menu.xml | 5 + .../main/res/values/string-untranslated.xml | 4 +- 12 files changed, 364 insertions(+), 48 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/privacy/ui/WhitelistViewModelTest.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt b/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt index 991e29315217..a566fb3e778f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt @@ -17,31 +17,103 @@ package com.duckduckgo.app.brokensite import com.duckduckgo.app.global.model.Site -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.whenever -import org.junit.Assert.assertEquals +import com.duckduckgo.app.global.model.SiteMonitor +import com.duckduckgo.app.surrogates.SurrogateResponse +import com.duckduckgo.app.trackerdetection.model.TrackingEvent +import org.junit.Assert.* import org.junit.Test class BrokenSiteDataTest { @Test - fun whenSiteIsNotNullThenBrokenSiteDataContainsUrl() { - val site = buildSite("foo.com") + fun whenSiteIsNullThenDataIsEmptyAndUpgradedIsFalse() { + val data = BrokenSiteData.fromSite(null) + assertTrue(data.url.isEmpty()) + assertTrue(data.blockedTrackers.isEmpty()) + assertTrue(data.surrogates.isEmpty()) + assertFalse(data.upgradedToHttps) + } + + @Test + fun whenSiteExistsThenDataContainsUrl() { + val site = buildSite(SITE_URL) val data = BrokenSiteData.fromSite(site) - assertEquals("foo.com", data.url) + assertEquals(SITE_URL, data.url) } @Test - fun whenSiteIsNullThenBrokenSiteDataContainsBlankUrl() { - val data = BrokenSiteData.fromSite(null) - assertEquals("", data.url) + fun whenSiteUpgradedThenHttpsUpgradedIsTrue() { + val site = buildSite(SITE_URL, httpsUpgraded = true) + val data = BrokenSiteData.fromSite(site) + assertTrue(data.upgradedToHttps) + } + + @Test + fun whenSiteNotUpgradedThenHttpsUpgradedIsFalse() { + val site = buildSite(SITE_URL, httpsUpgraded = false) + val data = BrokenSiteData.fromSite(site) + assertFalse(data.upgradedToHttps) + } + + @Test + fun whenSiteHasNoTrackersThenBlockedTrackersIsEmpty() { + val site = buildSite(SITE_URL) + val data = BrokenSiteData.fromSite(site) + assertTrue(data.blockedTrackers.isEmpty()) + } + + @Test + fun whenSiteHasBlockedTrackersThenBlockedTrackersExist() { + val site = buildSite(SITE_URL) + val event = TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", emptyList(), null, false) + val anotherEvent = TrackingEvent("http://www.example.com/test", "http://www.anothertracker.com/tracker.js", emptyList(), null, false) + site.trackerDetected(event) + site.trackerDetected(anotherEvent) + assertEquals("www.tracker.com,www.anothertracker.com", BrokenSiteData.fromSite(site).blockedTrackers) + } + + @Test + fun whenSiteHadSameHostBlockedTrackersThenOnlyUniqueTrackersIncludedInData() { + val site = buildSite(SITE_URL) + val event = TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", emptyList(), null, false) + val anotherEvent = TrackingEvent("http://www.example.com/test", "http://www.tracker.com/tracker2.js", emptyList(), null, false) + site.trackerDetected(event) + site.trackerDetected(anotherEvent) + assertEquals("www.tracker.com", BrokenSiteData.fromSite(site).blockedTrackers) + } + + @Test + fun whenSiteHasNoSurrogatesThenSurrogatesIsEmpty() { + val site = buildSite(SITE_URL) + val data = BrokenSiteData.fromSite(site) + assertTrue(data.surrogates.isEmpty()) + } + + @Test + fun whenSiteHasSurrogatesThenSurrogatesExist() { + val surrogate = SurrogateResponse(true, "surrogate.com/test.js", "", "") + val anotherSurrogate = SurrogateResponse(true, "anothersurrogate.com/test.js", "", "") + val site = buildSite(SITE_URL) + site.surrogateDetected(surrogate) + site.surrogateDetected(anotherSurrogate) + assertEquals("surrogate.com,anothersurrogate.com", BrokenSiteData.fromSite(site).surrogates) + } + + @Test + fun whenSiteHasSameHostSurrogatesThenOnlyUniqueSurrogateIncludedInData() { + val surrogate = SurrogateResponse(true, "surrogate.com/test.js", "", "") + val anotherSurrogate = SurrogateResponse(true, "surrogate.com/test2.js", "", "") + val site = buildSite(SITE_URL) + site.surrogateDetected(surrogate) + site.surrogateDetected(anotherSurrogate) + assertEquals("surrogate.com", BrokenSiteData.fromSite(site).surrogates) + } + + private fun buildSite(url: String, httpsUpgraded: Boolean = false): Site { + return SiteMonitor(url, "", upgradedHttps = httpsUpgraded) } - private fun buildSite( - url: String - ): Site { - val site: Site = mock() - whenever(site.url).thenReturn(url) - return site + companion object { + private const val SITE_URL = "foo.com" } } \ No newline at end of file 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 9d2cea930704..48760f43efdd 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -51,7 +51,6 @@ import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.global.db.AppDatabase -import com.duckduckgo.app.cta.ui.HomeTopPanelCta import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.onboarding.store.OnboardingStore @@ -60,6 +59,7 @@ import com.duckduckgo.app.privacy.db.NetworkLeaderboardDao import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.TestEntity +import com.duckduckgo.app.privacy.model.UserWhitelistedDomain import com.duckduckgo.app.runBlocking import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.VariantManager @@ -222,7 +222,7 @@ class BrowserTabViewModelTest { whenever(mockTabsRepository.retrieveSiteData(any())).thenReturn(MutableLiveData()) whenever(mockPrivacyPractices.privacyPracticesFor(any())).thenReturn(PrivacyPractices.UNKNOWN) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) - whenever(mockUserWhitelistDao.contains(anyString())).thenReturn(true) + whenever(mockUserWhitelistDao.contains(anyString())).thenReturn(false) testee = BrowserTabViewModel( statisticsUpdater = mockStatisticsUpdater, @@ -1062,11 +1062,23 @@ class BrowserTabViewModelTest { } @Test - fun whenUserSelectsToShareLinkThenShareLinkCommandSent() { - loadUrl("foo.com") - testee.onShareSelected() - val command = captureCommands().value as Command.ShareLink - assertEquals("foo.com", command.url) + fun whenUserTogglesNonWhitelistedSiteThenSiteAddedToWhitelistAndPixelSentAndPageRefreshed() = coroutineRule.runBlocking { + whenever(mockUserWhitelistDao.contains("www.example.com")).thenReturn(false) + loadUrl("http://www.example.com/home.html") + testee.onWhitelistSelected() + verify(mockUserWhitelistDao).insert(UserWhitelistedDomain("www.example.com")) + verify(mockPixel).fire(Pixel.PixelName.BROWSER_MENU_WHITELIST_ADD) + verify(mockCommandObserver).onChanged(Command.Refresh) + } + + @Test + fun whenUserTogglesWhitelsitedSiteThenSiteRemovedFromWhitelistAndPixelSentAndPageRefreshed() = coroutineRule.runBlocking { + whenever(mockUserWhitelistDao.contains("www.example.com")).thenReturn(true) + loadUrl("http://www.example.com/home.html") + testee.onWhitelistSelected() + verify(mockUserWhitelistDao).delete(UserWhitelistedDomain("www.example.com")) + verify(mockPixel).fire(Pixel.PixelName.BROWSER_MENU_WHITELIST_REMOVE) + verify(mockCommandObserver).onChanged(Command.Refresh) } @Test @@ -1084,6 +1096,14 @@ class BrowserTabViewModelTest { assertEquals("", command.data.url) } + @Test + fun whenUserSelectsToShareLinkThenShareLinkCommandSent() { + loadUrl("foo.com") + testee.onShareSelected() + val command = captureCommands().value as Command.ShareLink + assertEquals("foo.com", command.url) + } + @Test fun whenUserSelectsToShareLinkWithNullUrlThenShareLinkCommandNotSent() { loadUrl(null) diff --git a/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/WhitelistViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/WhitelistViewModelTest.kt new file mode 100644 index 000000000000..e4e702f9a44b --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/privacy/ui/WhitelistViewModelTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2020 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.privacy.ui + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.duckduckgo.app.CoroutineTestRule +import com.duckduckgo.app.privacy.db.UserWhitelistDao +import com.duckduckgo.app.privacy.model.UserWhitelistedDomain +import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command +import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.ShowAdd +import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.ShowWhitelistFormatError +import com.duckduckgo.app.runBlocking +import com.nhaarman.mockitokotlin2.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.After +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class WhitelistViewModelTest { + + @ExperimentalCoroutinesApi + @get:Rule + var coroutineRule = CoroutineTestRule() + + @get:Rule + @Suppress("unused") + var instantTaskExecutorRule = InstantTaskExecutorRule() + + private val mockDao: UserWhitelistDao = mock() + private val liveData = MutableLiveData>() + + private val mockCommandObserver: Observer = mock() + private var commandCaptor: KArgumentCaptor = argumentCaptor() + + private val testee by lazy { WhitelistViewModel(mockDao, coroutineRule.testDispatcherProvider) } + + @Before + fun before() = coroutineRule.runBlocking { + liveData.value = emptyList() + whenever(mockDao.all()).thenReturn(liveData) + testee.command.observeForever(mockCommandObserver) + } + + @After + fun after() { + testee.command.removeObserver(mockCommandObserver) + } + + @Test + fun whenWhitelistUpdatedWithDataThenViewStateIsUpdatedAndWhitelistDisplayed() { + val list = listOf(UserWhitelistedDomain(DOMAIN), UserWhitelistedDomain(NEW_DOMAIN)) + liveData.postValue(list) + val viewState = testee.viewState.value!! + assertEquals(list, viewState.whitelist) + assertTrue(viewState.showWhitelist) + } + + @Test + fun whenWhitelistUpdatedWithEmptyListThenViewStateIsUpdatedAndWhitelistNotDisplayed() { + liveData.postValue(emptyList()) + val viewState = testee.viewState.value!! + assertTrue(viewState.whitelist.isEmpty()) + assertFalse(viewState.showWhitelist) + } + + @Test + fun whenAddRequestedThenAddShown() { + testee.onAddRequested() + verify(mockCommandObserver).onChanged(ShowAdd) + } + + @Test + fun whenValidEntryAddedThenDaoUpdated() { + val entry = UserWhitelistedDomain(NEW_DOMAIN) + testee.onEntryAdded(entry) + verify(mockDao).insert(entry) + } + + @Test + fun whenInvalidEntryAddedThenErrorShownAndDaoNotUpdated() { + val entry = UserWhitelistedDomain(INVALID_DOMAIN) + testee.onEntryAdded(entry) + verify(mockCommandObserver).onChanged(ShowWhitelistFormatError) + verify(mockDao, never()).insert(entry) + } + + @Test + fun whenEditRequestedThenEditShown() { + val entry = UserWhitelistedDomain(DOMAIN) + testee.onEditRequested(entry) + verify(mockCommandObserver).onChanged(commandCaptor.capture()) + val lastValue = commandCaptor.lastValue as Command.ShowEdit + assertEquals(entry, lastValue.entry) + } + + @Test + fun whenValidEditSubmittedThenDaoUpdated() { + val old = UserWhitelistedDomain(DOMAIN) + val new = UserWhitelistedDomain(NEW_DOMAIN) + testee.onEntryEdited(old, new) + verify(mockDao).delete(old) + verify(mockDao).insert(new) + } + + @Test + fun whenValidEditSubmittedThenErrorShownAndDaoNotUpdated() { + val old = UserWhitelistedDomain(DOMAIN) + val new = UserWhitelistedDomain(INVALID_DOMAIN) + testee.onEntryEdited(old, new) + verify(mockCommandObserver).onChanged(ShowWhitelistFormatError) + verify(mockDao, never()).delete(old) + verify(mockDao, never()).insert(new) + } + + @Test + fun whenDeleteRequestedThenDeletionConfirmed() { + val entry = UserWhitelistedDomain(DOMAIN) + testee.onDeleteRequested(entry) + verify(mockCommandObserver).onChanged(commandCaptor.capture()) + val lastValue = commandCaptor.lastValue as Command.ConfirmDelete + assertEquals(entry, lastValue.entry) + } + + @Test + fun whenDeletedThenDaoUpdated() { + val entry = UserWhitelistedDomain(DOMAIN) + testee.onEntryDeleted(entry) + verify(mockDao).delete(entry) + } + + companion object { + private const val DOMAIN = "example.com" + private const val NEW_DOMAIN = "new.example.com" + private const val INVALID_DOMAIN = "_" + } +} \ No newline at end of file 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 19fc9cba9844..7ff98451a47e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1283,6 +1283,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } onMenuItemClicked(view.addBookmarksPopupMenuItem) { launch { viewModel.onBookmarkAddRequested() } } onMenuItemClicked(view.findInPageMenuItem) { viewModel.onFindInPageSelected() } + onMenuItemClicked(view.whitelistPopupMenuItem) { viewModel.onWhitelistSelected() } onMenuItemClicked(view.brokenSitePopupMenuItem) { viewModel.onBrokenSiteSelected() } onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() } onMenuItemClicked(view.requestDesktopSiteCheckMenuItem) { viewModel.onDesktopSiteModeToggled(view.requestDesktopSiteCheckMenuItem.isChecked) } @@ -1317,6 +1318,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } onMenuItemClicked(view.addBookmarksPopupMenuItem) { launch { viewModel.onBookmarkAddRequested() } } onMenuItemClicked(view.findInPageMenuItem) { viewModel.onFindInPageSelected() } + onMenuItemClicked(view.whitelistPopupMenuItem) { viewModel.onWhitelistSelected() } onMenuItemClicked(view.brokenSitePopupMenuItem) { viewModel.onBrokenSiteSelected() } onMenuItemClicked(view.settingsPopupMenuItem) { browserActivity?.launchSettings() } onMenuItemClicked(view.requestDesktopSiteCheckMenuItem) { viewModel.onDesktopSiteModeToggled(view.requestDesktopSiteCheckMenuItem.isChecked) } @@ -1613,6 +1615,8 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi newTabPopupMenuItem.isEnabled = browserShowing addBookmarksPopupMenuItem?.isEnabled = viewState.canAddBookmarks sharePageMenuItem?.isEnabled = viewState.canSharePage + whitelistPopupMenuItem?.isEnabled = viewState.canWhitelist + whitelistPopupMenuItem?.text = getText(if (viewState.isWhitelisted) R.string.whitelistRemove else R.string.whitelistAdd) brokenSitePopupMenuItem?.isEnabled = viewState.canReportSite requestDesktopSiteCheckMenuItem?.isEnabled = viewState.canChangeBrowsingMode 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 a0c4105d57f5..11bea8d665e1 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -28,6 +28,7 @@ import android.webkit.WebChromeClient import android.webkit.WebView import androidx.annotation.AnyThread import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -80,7 +81,7 @@ import com.jakewharton.rxrelay2.PublishRelay import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import kotlinx.android.synthetic.main.include_omnibar_toolbar.* +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -135,6 +136,8 @@ class BrowserTabViewModel( val canAddBookmarks: Boolean = false, val canGoBack: Boolean = false, val canGoForward: Boolean = false, + val canWhitelist: Boolean = false, + val isWhitelisted: Boolean = false, val canReportSite: Boolean = false, val addToHomeEnabled: Boolean = false, val addToHomeVisible: Boolean = false @@ -493,7 +496,10 @@ class BrowserTabViewModel( val currentOmnibarViewState = currentOmnibarViewState() omnibarViewState.value = currentOmnibarViewState.copy(omnibarText = omnibarTextForUrl(url), shouldMoveCaretToEnd = false) val currentBrowserViewState = currentBrowserViewState() + val domain = site?.domain + val canWhitelist = domain != null findInPageViewState.value = FindInPageViewState(visible = false, canFindInPage = true) + browserViewState.value = currentBrowserViewState.copy( browserShowing = true, canAddBookmarks = true, @@ -502,6 +508,8 @@ class BrowserTabViewModel( canSharePage = true, showPrivacyGrade = true, canReportSite = true, + canWhitelist = canWhitelist, + isWhitelisted = false, showSearchIcon = false, showClearButton = false ) @@ -513,7 +521,7 @@ class BrowserTabViewModel( } updateLoadingStatePrivacy() - + domain?.let { updateWhitelistedState(domain) } registerSiteVisit() } @@ -525,6 +533,14 @@ class BrowserTabViewModel( } } + private fun updateWhitelistedState(domain: String) { + viewModelScope.launch(dispatchers.io()) { + browserViewState.postValue( + currentBrowserViewState().copy(isWhitelisted = userWhitelistDao.contains(domain)) + ) + } + } + private fun urlUpdated(url: String) { Timber.v("Page url updated: $url") site?.url = url @@ -731,6 +747,32 @@ class BrowserTabViewModel( command.value = BrokenSiteFeedback(BrokenSiteData.fromSite(site)) } + fun onWhitelistSelected() { + val domain = site?.domain ?: return + GlobalScope.launch(dispatchers.io()) { + if (userWhitelistDao.contains(domain)) { + removeFromWhitelist(domain) + } else { + addToWhitelist(domain) + } + command.postValue(Refresh) + } + } + + @WorkerThread + private fun addToWhitelist(domain: String) { + pixel.fire(PixelName.BROWSER_MENU_WHITELIST_ADD) + userWhitelistDao.insert(domain) + browserViewState.postValue(currentBrowserViewState().copy(isWhitelisted = true)) + } + + @WorkerThread + private fun removeFromWhitelist(domain: String) { + pixel.fire(PixelName.BROWSER_MENU_WHITELIST_REMOVE) + userWhitelistDao.delete(domain) + browserViewState.postValue(currentBrowserViewState().copy(isWhitelisted = false)) + } + fun onUserSelectedToEditQuery(query: String) { omnibarViewState.value = currentOmnibarViewState().copy(isEditing = false, omnibarText = query, shouldMoveCaretToEnd = true) autoCompleteViewState.value = AutoCompleteViewState(showSuggestions = false) diff --git a/app/src/main/java/com/duckduckgo/app/global/UriString.kt b/app/src/main/java/com/duckduckgo/app/global/UriString.kt index 780fe67451eb..49b1d851d7fb 100644 --- a/app/src/main/java/com/duckduckgo/app/global/UriString.kt +++ b/app/src/main/java/com/duckduckgo/app/global/UriString.kt @@ -25,7 +25,8 @@ class UriString { private const val localhost = "localhost" private const val space = " " - private val webUrlRegex = PatternsCompat.WEB_URL.toRegex() + private val webUrlRegex by lazy { PatternsCompat.WEB_URL.toRegex() } + private val domainRegex by lazy { PatternsCompat.DOMAIN_NAME.toRegex() } fun host(uriString: String): String? { return Uri.parse(uriString).baseHost @@ -42,17 +43,16 @@ class UriString { val uri = Uri.parse(inputQuery).withScheme() if (uri.scheme != UrlScheme.http && uri.scheme != UrlScheme.https) return false if (uri.userInfo != null) return false - if (uri.host == null) return false - return isValidHost(uri.host) - } - private fun isValidHost(host: String): Boolean { + val host = uri.host ?: return false if (host == localhost) return true if (host.contains(space)) return false if (host.contains("!")) return false + return (webUrlRegex.containsMatchIn(host)) + } - if (webUrlRegex.containsMatchIn(host)) return true - return false + fun isValidDomain(domain: String): Boolean { + return domainRegex.matches(domain) } } } diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt index 97451ae07864..56f9e9fb65ce 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt @@ -21,9 +21,7 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.* -import android.widget.ImageView -import android.widget.PopupMenu -import android.widget.TextView +import android.widget.* import androidx.lifecycle.Observer import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.RecyclerView @@ -35,12 +33,12 @@ import com.duckduckgo.app.global.view.gone import com.duckduckgo.app.global.view.html import com.duckduckgo.app.global.view.show import com.duckduckgo.app.privacy.model.UserWhitelistedDomain -import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.ConfirmDelete -import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.ShowEdit +import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.* import kotlinx.android.synthetic.main.activity_whitelist.* import kotlinx.android.synthetic.main.edit_whitelist.* import kotlinx.android.synthetic.main.include_toolbar.* import kotlinx.android.synthetic.main.view_whitelist_entry.view.* +import org.jetbrains.anko.toast class WhitelistActivity : DuckDuckGoActivity() { @@ -52,7 +50,7 @@ class WhitelistActivity : DuckDuckGoActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_whitelist) - setupActionBar() + setupToolbar(toolbar) setupRecycler() observeViewModel() } @@ -77,11 +75,6 @@ class WhitelistActivity : DuckDuckGoActivity() { recycler.addItemDecoration(separator) } - private fun setupActionBar() { - setSupportActionBar(toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - } - private fun observeViewModel() { viewModel.viewState.observe(this, Observer { viewState -> viewState?.let { renderViewState(it) } @@ -114,9 +107,10 @@ class WhitelistActivity : DuckDuckGoActivity() { private fun processCommand(command: WhitelistViewModel.Command?) { when (command) { - is WhitelistViewModel.Command.ShowAdd -> showAddDialog() + is ShowAdd -> showAddDialog() is ShowEdit -> showEditDialog(command.entry) is ConfirmDelete -> showDeleteDialog(command.entry) + is ShowWhitelistFormatError -> showWhitelistFormatError() } } @@ -139,10 +133,9 @@ class WhitelistActivity : DuckDuckGoActivity() { private fun showEditDialog(entry: UserWhitelistedDomain) { val editDialog = AlertDialog.Builder(this).apply { setTitle(R.string.dialogEditTitle) - textInput.setText(entry.domain) setView(R.layout.edit_whitelist) setPositiveButton(android.R.string.yes) { _, _ -> - val newText = textInput.text.toString() + val newText = dialog?.textInput?.text.toString() viewModel.onEntryEdited(entry, UserWhitelistedDomain(newText)) } setNegativeButton(android.R.string.no) { _, _ -> } @@ -151,6 +144,9 @@ class WhitelistActivity : DuckDuckGoActivity() { dialog?.dismiss() dialog = editDialog editDialog.show() + + editDialog.textInput.setText(entry.domain) + editDialog.textInput.setSelection(entry.domain.length) } private fun showDeleteDialog(entry: UserWhitelistedDomain) { @@ -166,6 +162,10 @@ class WhitelistActivity : DuckDuckGoActivity() { deleteDialog.show() } + private fun showWhitelistFormatError() { + Toast.makeText(this, R.string.whitelistFormatError, Toast.LENGTH_LONG).show() + } + override fun onDestroy() { dialog?.dismiss() super.onDestroy() diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt index 28589c65540b..da281eb098c7 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModel import com.duckduckgo.app.global.DefaultDispatcherProvider import com.duckduckgo.app.global.DispatcherProvider import com.duckduckgo.app.global.SingleLiveEvent +import com.duckduckgo.app.global.UriString import com.duckduckgo.app.privacy.db.UserWhitelistDao import com.duckduckgo.app.privacy.model.UserWhitelistedDomain import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.* @@ -32,7 +33,6 @@ import kotlinx.coroutines.launch class WhitelistViewModel( private val dao: UserWhitelistDao, private val dispatchers: DispatcherProvider = DefaultDispatcherProvider() - ) : ViewModel() { data class ViewState( @@ -44,6 +44,7 @@ class WhitelistViewModel( object ShowAdd : Command() class ShowEdit(val entry: UserWhitelistedDomain) : Command() class ConfirmDelete(val entry: UserWhitelistedDomain) : Command() + object ShowWhitelistFormatError : Command() } val viewState: MutableLiveData = MutableLiveData() @@ -74,6 +75,10 @@ class WhitelistViewModel( } fun onEntryAdded(entry: UserWhitelistedDomain) { + if (!UriString.isValidDomain(entry.domain)) { + command.value = ShowWhitelistFormatError + return + } GlobalScope.launch(dispatchers.io()) { dao.insert(entry) } @@ -84,6 +89,10 @@ class WhitelistViewModel( } fun onEntryEdited(old: UserWhitelistedDomain, new: UserWhitelistedDomain) { + if (!UriString.isValidDomain(new.domain)) { + command.value = ShowWhitelistFormatError + return + } onEntryDeleted(old) onEntryAdded(new) } diff --git a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index 10c74923d458..2e81ca74f834 100644 --- a/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -74,7 +74,10 @@ interface Pixel { PRIVACY_DASHBOARD_WHITELIST_REMOVE("mp_wlr"), PRIVACY_DASHBOARD_MANAGE_WHITELIST("mp_mw"), PRIVACY_DASHBOARD_REPORT_BROKEN_SITE("mp_rb"), - + + BROWSER_MENU_WHITELIST_ADD("mb_wla"), + BROWSER_MENU_WHITELIST_REMOVE("mb_wlr"), + HTTPS_NO_LOOKUP("m_https_nl"), HTTPS_LOCAL_UPGRADE("m_https_lu"), HTTPS_SERVICE_REQUEST_UPGRADE("m_https_sru"), diff --git a/app/src/main/res/layout/popup_window_browser_bottom_tab_menu.xml b/app/src/main/res/layout/popup_window_browser_bottom_tab_menu.xml index e267ce816c83..ccbaad57f3b7 100644 --- a/app/src/main/res/layout/popup_window_browser_bottom_tab_menu.xml +++ b/app/src/main/res/layout/popup_window_browser_bottom_tab_menu.xml @@ -76,6 +76,11 @@ android:paddingEnd="0dp" android:text="@string/requestDesktopSiteMenuTitle" /> + + + + Whitelist + Add to Whitelist + Remove from Whitelist More options for whitelist entry %s - Add to Whitelist Are you sure you want to delete <b>%s</b> from whitelist? These whitelisted sites will not be upgraded by Privacy Protection No whitelisted sites yet www.example.com + Could not save, enter a domain like example.com Privacy Protection Whitelist From adfb5f2c7e90d58633899cd7e605cf73cc6c55e4 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 4 May 2020 19:51:43 +0100 Subject: [PATCH 07/15] Lint tidy up --- .../java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt | 2 +- app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt | 1 - app/src/main/java/com/duckduckgo/app/di/VariantModule.kt | 4 ---- .../main/java/com/duckduckgo/app/launch/LaunchViewModel.kt | 1 - .../com/duckduckgo/app/privacy/model/UserWhitelistedDomain.kt | 1 - .../java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt | 1 - 6 files changed, 1 insertion(+), 9 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt b/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt index a566fb3e778f..77e7ec14bb36 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/brokensite/BrokenSiteDataTest.kt @@ -73,7 +73,7 @@ class BrokenSiteDataTest { } @Test - fun whenSiteHadSameHostBlockedTrackersThenOnlyUniqueTrackersIncludedInData() { + fun whenSiteHasSameHostBlockedTrackersThenOnlyUniqueTrackersIncludedInData() { val site = buildSite(SITE_URL) val event = TrackingEvent("http://www.example.com", "http://www.tracker.com/tracker.js", emptyList(), null, false) val anotherEvent = TrackingEvent("http://www.example.com/test", "http://www.tracker.com/tracker2.js", emptyList(), null, false) diff --git a/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt b/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt index 77b56b0bbc73..d7e1a411af07 100644 --- a/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/PrivacyModule.kt @@ -32,7 +32,6 @@ import com.duckduckgo.app.tabs.model.TabRepository import com.duckduckgo.app.trackerdetection.TdsEntityLookup import com.duckduckgo.app.trackerdetection.db.TdsDomainEntityDao import com.duckduckgo.app.trackerdetection.db.TdsEntityDao -import com.duckduckgo.app.trackerdetection.model.TdsEntity import dagger.Module import dagger.Provides import javax.inject.Singleton diff --git a/app/src/main/java/com/duckduckgo/app/di/VariantModule.kt b/app/src/main/java/com/duckduckgo/app/di/VariantModule.kt index 74c1b42c3469..f0c56a1023b4 100644 --- a/app/src/main/java/com/duckduckgo/app/di/VariantModule.kt +++ b/app/src/main/java/com/duckduckgo/app/di/VariantModule.kt @@ -16,10 +16,6 @@ package com.duckduckgo.app.di -import android.content.Context -import com.duckduckgo.app.global.shortcut.AppShortcutCreator -import com.duckduckgo.app.icon.api.AppIconModifier -import com.duckduckgo.app.icon.api.IconModifier import com.duckduckgo.app.statistics.ExperimentationVariantManager import com.duckduckgo.app.statistics.VariantManager import com.duckduckgo.app.statistics.WeightedRandomizer diff --git a/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt b/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt index eb1b2f4ac2b8..5127d442a0ff 100644 --- a/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt @@ -18,7 +18,6 @@ package com.duckduckgo.app.launch import androidx.lifecycle.ViewModel import com.duckduckgo.app.global.SingleLiveEvent -import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.store.isNewUser import com.duckduckgo.app.referral.AppInstallationReferrerStateListener diff --git a/app/src/main/java/com/duckduckgo/app/privacy/model/UserWhitelistedDomain.kt b/app/src/main/java/com/duckduckgo/app/privacy/model/UserWhitelistedDomain.kt index 2185c58cb2ac..e39dd5729a71 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/model/UserWhitelistedDomain.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/model/UserWhitelistedDomain.kt @@ -18,7 +18,6 @@ package com.duckduckgo.app.privacy.model import androidx.room.Entity import androidx.room.PrimaryKey -import com.duckduckgo.app.trackerdetection.model.DomainContainer @Entity(tableName = "user_whitelist") data class UserWhitelistedDomain( diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt index 56f9e9fb65ce..bc9ea572a07d 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistActivity.kt @@ -38,7 +38,6 @@ import kotlinx.android.synthetic.main.activity_whitelist.* import kotlinx.android.synthetic.main.edit_whitelist.* import kotlinx.android.synthetic.main.include_toolbar.* import kotlinx.android.synthetic.main.view_whitelist_entry.view.* -import org.jetbrains.anko.toast class WhitelistActivity : DuckDuckGoActivity() { From bf5197d4be0ce39450b6ee2d4aa28e83c0b30b51 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Wed, 6 May 2020 10:09:10 +0100 Subject: [PATCH 08/15] Add icon tint --- app/src/main/res/layout/view_whitelist_entry.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/view_whitelist_entry.xml b/app/src/main/res/layout/view_whitelist_entry.xml index 85777d7191cc..9bb256cc8260 100644 --- a/app/src/main/res/layout/view_whitelist_entry.xml +++ b/app/src/main/res/layout/view_whitelist_entry.xml @@ -56,6 +56,7 @@ android:paddingEnd="14dp" android:scaleType="center" android:src="@drawable/ic_overflow_24dp" + app:tint="?toolbarIconColor" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" From 7b271e704c1d0d37b556d116679a06740b3362a7 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 8 May 2020 01:23:56 +0100 Subject: [PATCH 09/15] Fix double pixel when activities not kept --- .../duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt | 8 +++++--- .../app/privacy/ui/PrivacyDashboardViewModel.kt | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt index 9c6d21d96110..aa7ff261a10b 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardActivity.kt @@ -49,6 +49,7 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { @Inject lateinit var repository: TabRepository + @Inject lateinit var pixel: Pixel @@ -99,16 +100,17 @@ class PrivacyDashboardActivity : DuckDuckGoActivity() { if (isFinishing) { return } - privacyBanner.setImageResource(viewState.afterGrade.banner(viewState.toggleEnabled)) + val toggle = viewState.toggleEnabled ?: true + privacyBanner.setImageResource(viewState.afterGrade.banner(toggle)) domain.text = viewState.domain - heading.text = upgradeRenderer.heading(this, viewState.beforeGrade, viewState.afterGrade, viewState.toggleEnabled).html(this) + heading.text = upgradeRenderer.heading(this, viewState.beforeGrade, viewState.afterGrade, toggle).html(this) httpsIcon.setImageResource(viewState.httpsStatus.icon()) httpsText.text = viewState.httpsStatus.text(this) networksIcon.setImageResource(trackersRenderer.networksIcon(viewState.allTrackersBlocked)) networksText.text = trackersRenderer.trackersText(this, viewState.trackerCount, viewState.allTrackersBlocked) practicesIcon.setImageResource(viewState.practices.icon()) practicesText.text = viewState.practices.text(this) - renderToggle(viewState.toggleEnabled) + renderToggle(toggle) renderTrackerNetworkLeaderboard(viewState) updateActivityResult(viewState.shouldReloadPage) } diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt index b8e88881c8e6..2c752653af4b 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt @@ -56,7 +56,7 @@ class PrivacyDashboardViewModel( val trackerCount: Int, val allTrackersBlocked: Boolean, val practices: PrivacyPractices.Summary, - val toggleEnabled: Boolean, + val toggleEnabled: Boolean?, val shouldShowTrackerNetworkLeaderboard: Boolean, val sitesVisited: Int, val trackerNetworkEntries: List, @@ -130,7 +130,7 @@ class PrivacyDashboardViewModel( httpsStatus = HttpsStatus.SECURE, trackerCount = 0, allTrackersBlocked = true, - toggleEnabled = true, + toggleEnabled = null, practices = UNKNOWN, shouldShowTrackerNetworkLeaderboard = false, sitesVisited = 0, @@ -157,6 +157,10 @@ class PrivacyDashboardViewModel( } fun onPrivacyToggled(enabled: Boolean) { + if (viewState.value?.toggleEnabled == null) { + return + } + if (enabled == viewState.value?.toggleEnabled) { return } From 528924c517935726da4bc972542228319a2a713a Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Fri, 8 May 2020 15:00:10 +0100 Subject: [PATCH 10/15] Remove mixed viewstate set and post value and access of viewState from multiple threads --- .../privacy/ui/PrivacyDashboardViewModel.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt index 2c752653af4b..187da467e412 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt @@ -17,10 +17,7 @@ package com.duckduckgo.app.privacy.ui import androidx.annotation.VisibleForTesting -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModel +import androidx.lifecycle.* import com.duckduckgo.app.brokensite.BrokenSiteData import com.duckduckgo.app.global.DefaultDispatcherProvider import com.duckduckgo.app.global.DispatcherProvider @@ -40,6 +37,7 @@ import com.duckduckgo.app.statistics.pixels.Pixel import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.* import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class PrivacyDashboardViewModel( private val userWhitelistDao: UserWhitelistDao, @@ -141,18 +139,20 @@ class PrivacyDashboardViewModel( private fun updateSite(site: Site) { val grades = site.calculateGrades() - - GlobalScope.launch(dispatchers.io()) { - viewState.postValue(viewState.value?.copy( - domain = site.domain ?: "", - beforeGrade = grades.grade, - afterGrade = grades.improvedGrade, - httpsStatus = site.https, - trackerCount = site.trackerCount, - allTrackersBlocked = site.allTrackersBlocked, - toggleEnabled = site.domain?.let { !userWhitelistDao.contains(it) } ?: true, - practices = site.privacyPractices.summary - )) + viewModelScope.launch(dispatchers.io()) { + val toggleEnabled = site.domain?.let { !userWhitelistDao.contains(it) } ?: true + withContext(dispatchers.main()) { + viewState.value = viewState.value?.copy( + domain = site.domain ?: "", + beforeGrade = grades.grade, + afterGrade = grades.improvedGrade, + httpsStatus = site.https, + trackerCount = site.trackerCount, + allTrackersBlocked = site.allTrackersBlocked, + toggleEnabled = toggleEnabled, + practices = site.privacyPractices.summary + ) + } } } From 8bdbf4ae02f919b7dea0abb1d977ab43578739f7 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 11 May 2020 14:01:39 +0100 Subject: [PATCH 11/15] Update test to also check pixel --- .../java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt b/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt index 3ed1cc0ee0ff..8665ae0d0ff9 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/httpsupgrade/HttpsUpgraderTest.kt @@ -71,10 +71,11 @@ class HttpsUpgraderTest { } @Test - fun whenHttpDomainIsUserWhitelistedThenShouldNotUpgrade() { + fun whenHttpDomainIsUserWhitelistedThenShouldNotUpgradeAndNoLookupPixelIsSet() { whenever(mockUserWhitelistDao.contains("www.local.url")).thenReturn(true) bloomFilter.add("www.local.url") assertFalse(testee.shouldUpgrade(Uri.parse("http://www.local.url"))) + mockPixel.fire(Pixel.PixelName.HTTPS_NO_LOOKUP) } @Test From 356b1d866d7943d47905f8f31974d826b1b8c9d2 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 11 May 2020 14:50:39 +0100 Subject: [PATCH 12/15] Cleanup unneeded annotations and client definition --- .../java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt | 2 -- app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt index b101953366cf..5080e65615a0 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt @@ -16,7 +16,6 @@ package com.duckduckgo.app.privacy.ui -import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -82,7 +81,6 @@ class ScorecardViewModel( ) } - @WorkerThread private fun updateSite(site: Site) { val domain = site.domain ?: "" val grades = site.calculateGrades() diff --git a/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt b/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt index f63036073aa4..43a6014137a8 100644 --- a/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt +++ b/app/src/main/java/com/duckduckgo/app/trackerdetection/Client.kt @@ -27,7 +27,6 @@ interface Client { // current clients TDS(ClientType.BLOCKING), TEMPORARY_WHITELIST(ClientType.WHITELIST), - USER_WHITELIST(ClientType.WHITELIST), // legacy clients EASYLIST(ClientType.BLOCKING), From 7e1862379240d72cd96a05d80fc775c90ab0daae Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 11 May 2020 19:26:46 +0100 Subject: [PATCH 13/15] Make db writes suspend functions to avoid future use without considering async nature --- .../app/privacy/ui/WhitelistViewModel.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt index da281eb098c7..3e8686b343f6 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt @@ -80,7 +80,7 @@ class WhitelistViewModel( return } GlobalScope.launch(dispatchers.io()) { - dao.insert(entry) + addEntryToDatabase(entry) } } @@ -93,8 +93,10 @@ class WhitelistViewModel( command.value = ShowWhitelistFormatError return } - onEntryDeleted(old) - onEntryAdded(new) + GlobalScope.launch(dispatchers.io()) { + deleteEntryFromDatabase(old) + addEntryToDatabase(new) + } } fun onDeleteRequested(entry: UserWhitelistedDomain) { @@ -103,7 +105,16 @@ class WhitelistViewModel( fun onEntryDeleted(entry: UserWhitelistedDomain) { GlobalScope.launch(dispatchers.io()) { - dao.delete(entry) + deleteEntryFromDatabase(entry) } + + } + + private suspend fun addEntryToDatabase(entry: UserWhitelistedDomain) { + dao.insert(entry) + } + + private suspend fun deleteEntryFromDatabase(entry: UserWhitelistedDomain) { + dao.delete(entry) } } \ No newline at end of file From 98e88f15de28dffd93557e246132954b43cc78c2 Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 11 May 2020 20:18:05 +0100 Subject: [PATCH 14/15] Update coroutines to use suspend functions for private methods and jump back to main thread to avoid posting live data --- .../app/browser/BrowserTabViewModel.kt | 47 ++++++++++--------- .../app/privacy/ui/ScorecardViewModel.kt | 35 +++++++------- .../app/privacy/ui/WhitelistViewModel.kt | 1 - 3 files changed, 43 insertions(+), 40 deletions(-) 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 11bea8d665e1..47cdb0d64141 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -28,7 +28,6 @@ import android.webkit.WebChromeClient import android.webkit.WebView import androidx.annotation.AnyThread import androidx.annotation.VisibleForTesting -import androidx.annotation.WorkerThread import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -520,24 +519,22 @@ class BrowserTabViewModel( statisticsUpdater.refreshSearchRetentionAtb() } - updateLoadingStatePrivacy() - domain?.let { updateWhitelistedState(domain) } + domain?.let { viewModelScope.launch { updateLoadingStatePrivacy(domain) } } + domain?.let { viewModelScope.launch { updateWhitelistedState(domain) } } registerSiteVisit() } - private fun updateLoadingStatePrivacy() { - viewModelScope.launch(dispatchers.io()) { - site?.domain?.let { - loadingViewState.postValue(currentLoadingViewState().copy(privacyOn = !userWhitelistDao.contains(it))) - } + private suspend fun updateLoadingStatePrivacy(domain: String) { + val whitelisted = withContext(dispatchers.io()) { userWhitelistDao.contains(domain) } + withContext(dispatchers.main()) { + loadingViewState.value = currentLoadingViewState().copy(privacyOn = !whitelisted) } } - private fun updateWhitelistedState(domain: String) { - viewModelScope.launch(dispatchers.io()) { - browserViewState.postValue( - currentBrowserViewState().copy(isWhitelisted = userWhitelistDao.contains(domain)) - ) + private suspend fun updateWhitelistedState(domain: String) { + val isWhitelisted = withContext(dispatchers.io()) { userWhitelistDao.contains(domain) } + withContext(dispatchers.main()) { + browserViewState.value = currentBrowserViewState().copy(isWhitelisted = isWhitelisted) } } @@ -749,7 +746,7 @@ class BrowserTabViewModel( fun onWhitelistSelected() { val domain = site?.domain ?: return - GlobalScope.launch(dispatchers.io()) { + GlobalScope.launch(dispatchers.default()) { if (userWhitelistDao.contains(domain)) { removeFromWhitelist(domain) } else { @@ -759,18 +756,24 @@ class BrowserTabViewModel( } } - @WorkerThread - private fun addToWhitelist(domain: String) { + private suspend fun addToWhitelist(domain: String) { pixel.fire(PixelName.BROWSER_MENU_WHITELIST_ADD) - userWhitelistDao.insert(domain) - browserViewState.postValue(currentBrowserViewState().copy(isWhitelisted = true)) + withContext(dispatchers.io()) { + userWhitelistDao.insert(domain) + } + withContext(dispatchers.main()) { + browserViewState.value = currentBrowserViewState().copy(isWhitelisted = true) + } } - @WorkerThread - private fun removeFromWhitelist(domain: String) { + private suspend fun removeFromWhitelist(domain: String) { pixel.fire(PixelName.BROWSER_MENU_WHITELIST_REMOVE) - userWhitelistDao.delete(domain) - browserViewState.postValue(currentBrowserViewState().copy(isWhitelisted = false)) + withContext(dispatchers.io()) { + userWhitelistDao.delete(domain) + } + withContext(dispatchers.main()) { + browserViewState.value = currentBrowserViewState().copy(isWhitelisted = false) + } } fun onUserSelectedToEditQuery(query: String) { diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt index 5080e65615a0..390813aff25f 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/ScorecardViewModel.kt @@ -29,6 +29,7 @@ import com.duckduckgo.app.privacy.model.PrivacyGrade import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.privacy.model.PrivacyPractices.Summary.UNKNOWN import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class ScorecardViewModel( private val userWhitelistDao: UserWhitelistDao, @@ -61,7 +62,7 @@ class ScorecardViewModel( if (site == null) { resetViewState() } else { - updateSite(site) + viewModelScope.launch { updateSite(site) } } } @@ -81,27 +82,27 @@ class ScorecardViewModel( ) } - private fun updateSite(site: Site) { + private suspend fun updateSite(site: Site) { val domain = site.domain ?: "" val grades = site.calculateGrades() val grade = grades.grade val improvedGrade = grades.improvedGrade - viewModelScope.launch(dispatchers.io()) { - viewState.postValue( - viewState.value?.copy( - domain = domain, - beforeGrade = grade, - afterGrade = improvedGrade, - trackerCount = site.trackerCount, - majorNetworkCount = site.majorNetworkCount, - httpsStatus = site.https, - allTrackersBlocked = site.allTrackersBlocked, - practices = site.privacyPractices.summary, - privacyOn = !userWhitelistDao.contains(domain), - showIsMemberOfMajorNetwork = site.entity?.isMajor ?: false, - showEnhancedGrade = grade != improvedGrade - ) + val isWhitelisted = withContext(dispatchers.io()) { userWhitelistDao.contains(domain) } + + withContext(dispatchers.main()) { + viewState.value = viewState.value?.copy( + domain = domain, + beforeGrade = grade, + afterGrade = improvedGrade, + trackerCount = site.trackerCount, + majorNetworkCount = site.majorNetworkCount, + httpsStatus = site.https, + allTrackersBlocked = site.allTrackersBlocked, + practices = site.privacyPractices.summary, + privacyOn = !isWhitelisted, + showIsMemberOfMajorNetwork = site.entity?.isMajor ?: false, + showEnhancedGrade = grade != improvedGrade ) } } diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt index 3e8686b343f6..9d6eb83ae924 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt @@ -107,7 +107,6 @@ class WhitelistViewModel( GlobalScope.launch(dispatchers.io()) { deleteEntryFromDatabase(entry) } - } private suspend fun addEntryToDatabase(entry: UserWhitelistedDomain) { From 6b9d6d667ca170616dd657140047810c6bb6095d Mon Sep 17 00:00:00 2001 From: Mia Alexiou Date: Mon, 11 May 2020 22:25:14 +0100 Subject: [PATCH 15/15] Extract repeated isWhitelisted check into own suspect function and other small tidy ups --- .../app/browser/BrowserTabViewModel.kt | 14 +++++--- .../privacy/ui/PrivacyDashboardViewModel.kt | 32 +++++++++---------- .../app/privacy/ui/WhitelistViewModel.kt | 5 +-- 3 files changed, 28 insertions(+), 23 deletions(-) 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 47cdb0d64141..9eb567382a69 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -525,19 +525,23 @@ class BrowserTabViewModel( } private suspend fun updateLoadingStatePrivacy(domain: String) { - val whitelisted = withContext(dispatchers.io()) { userWhitelistDao.contains(domain) } + val isWhitelisted = isWhitelisted(domain) withContext(dispatchers.main()) { - loadingViewState.value = currentLoadingViewState().copy(privacyOn = !whitelisted) + loadingViewState.value = currentLoadingViewState().copy(privacyOn = !isWhitelisted) } } private suspend fun updateWhitelistedState(domain: String) { - val isWhitelisted = withContext(dispatchers.io()) { userWhitelistDao.contains(domain) } + val isWhitelisted = isWhitelisted(domain) withContext(dispatchers.main()) { browserViewState.value = currentBrowserViewState().copy(isWhitelisted = isWhitelisted) } } + private suspend fun isWhitelisted(domain: String): Boolean { + return withContext(dispatchers.io()) { userWhitelistDao.contains(domain) } + } + private fun urlUpdated(url: String) { Timber.v("Page url updated: $url") site?.url = url @@ -746,8 +750,8 @@ class BrowserTabViewModel( fun onWhitelistSelected() { val domain = site?.domain ?: return - GlobalScope.launch(dispatchers.default()) { - if (userWhitelistDao.contains(domain)) { + GlobalScope.launch(dispatchers.io()) { + if (isWhitelisted(domain)) { removeFromWhitelist(domain) } else { addToWhitelist(domain) diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt index 187da467e412..038807ebe117 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/PrivacyDashboardViewModel.kt @@ -116,7 +116,7 @@ class PrivacyDashboardViewModel( if (site == null) { resetViewState() } else { - updateSite(site) + viewModelScope.launch { updateSite(site) } } } @@ -137,22 +137,22 @@ class PrivacyDashboardViewModel( ) } - private fun updateSite(site: Site) { + private suspend fun updateSite(site: Site) { val grades = site.calculateGrades() - viewModelScope.launch(dispatchers.io()) { - val toggleEnabled = site.domain?.let { !userWhitelistDao.contains(it) } ?: true - withContext(dispatchers.main()) { - viewState.value = viewState.value?.copy( - domain = site.domain ?: "", - beforeGrade = grades.grade, - afterGrade = grades.improvedGrade, - httpsStatus = site.https, - trackerCount = site.trackerCount, - allTrackersBlocked = site.allTrackersBlocked, - toggleEnabled = toggleEnabled, - practices = site.privacyPractices.summary - ) - } + val domain = site.domain ?: "" + val toggleEnabled = withContext(dispatchers.io()) { !userWhitelistDao.contains(domain) } + + withContext(dispatchers.main()) { + viewState.value = viewState.value?.copy( + domain = site.domain ?: "", + beforeGrade = grades.grade, + afterGrade = grades.improvedGrade, + httpsStatus = site.https, + trackerCount = site.trackerCount, + allTrackersBlocked = site.allTrackersBlocked, + toggleEnabled = toggleEnabled, + practices = site.privacyPractices.summary + ) } } diff --git a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt index 9d6eb83ae924..5d9dce0027cb 100644 --- a/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/privacy/ui/WhitelistViewModel.kt @@ -29,6 +29,7 @@ import com.duckduckgo.app.privacy.model.UserWhitelistedDomain import com.duckduckgo.app.privacy.ui.WhitelistViewModel.Command.* import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class WhitelistViewModel( private val dao: UserWhitelistDao, @@ -110,10 +111,10 @@ class WhitelistViewModel( } private suspend fun addEntryToDatabase(entry: UserWhitelistedDomain) { - dao.insert(entry) + withContext(dispatchers.io()) { dao.insert(entry) } } private suspend fun deleteEntryFromDatabase(entry: UserWhitelistedDomain) { - dao.delete(entry) + withContext(dispatchers.io()) { dao.delete(entry) } } } \ No newline at end of file