From 6c15dc142c1fa44c11f723960105aef149312458 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 4 Jul 2020 01:15:28 -0400 Subject: [PATCH 1/9] Adds sourceTabId property to TabEntity in order to track source tab to navigate back to. Bumps DB version and adds migration sql Adds viewModel method to close and return to source tab. Adds download callbacks to return to source tab if empty tab is opened. Specifically in this event when a download link opens a new blank tab and needs to close after download has been triggered. --- .../22.json | 726 ++++++++++++++++++ .../app/global/db/AppDatabaseTest.kt | 5 + .../app/browser/BrowserTabFragment.kt | 21 + .../app/browser/BrowserTabViewModel.kt | 11 + .../app/browser/BrowserViewModel.kt | 12 +- .../browser/DownloadConfirmationFragment.kt | 4 +- .../app/browser/WebViewClientListener.kt | 1 + .../app/browser/downloader/FileDownloader.kt | 2 + .../downloader/NetworkFileDownloader.kt | 1 + .../duckduckgo/app/global/db/AppDatabase.kt | 11 +- .../app/tabs/model/TabDataRepository.kt | 28 +- .../duckduckgo/app/tabs/model/TabEntitiy.kt | 3 +- .../app/tabs/model/TabRepository.kt | 6 +- 13 files changed, 822 insertions(+), 9 deletions(-) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json new file mode 100644 index 000000000000..f3e316f9b3b3 --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json @@ -0,0 +1,726 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "50ac3aaec4c827cc3366ecec3372c62b", + "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, `sourceTabId` 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 + }, + { + "fieldPath": "sourceTabId", + "columnName": "sourceTabId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "tabId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tabs_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `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 IF NOT EXISTS `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, `timestamp` INTEGER NOT NULL, `version` 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 + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "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": [] + }, + { + "tableName": "fireproofWebsites", + "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": [] + } + ], + "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, '50ac3aaec4c827cc3366ecec3372c62b')" + ] + } +} \ No newline at end of file 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 e82bc18113dd..01687530e83a 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 @@ -216,6 +216,11 @@ class AppDatabaseTest { createDatabaseAndMigrate(20, 21, migrationsProvider.MIGRATION_20_TO_21) } + @Test + fun whenMigratingFromVersion21To22ThenValidationSucceeds() { + createDatabaseAndMigrate(21, 22, migrationsProvider.MIGRATION_21_TO_22) + } + private fun createDatabase(version: Int) { testHelper.createDatabase(TEST_DB_NAME, version).close() } 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 6d0314188731..c4c252a56355 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1090,10 +1090,19 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } } + private fun closeAndReturnToSource() { + launch { + viewModel.closeAndSelectSourceTab() + } + } + private fun createDownloadListener(): FileDownloadListener { return object : FileDownloadListener { override fun downloadStarted() { fileDownloadNotificationManager.showDownloadInProgressNotification() + if (viewModel.url == null) { + closeAndReturnToSource() + } } override fun downloadFinished(file: File, mimeType: String?) { @@ -1117,6 +1126,18 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi snackbar.show() } + override fun downloadCancelled() { + if (viewModel.url == null) { + closeAndReturnToSource() + } + } + + override fun downloadOpened() { + if (viewModel.url == null) { + closeAndReturnToSource() + } + } + private fun showDownloadManagerAppSettings() { try { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 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 7ebae5877891..9bad69e581e0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -481,6 +481,17 @@ class BrowserTabViewModel( viewModelScope.launch { removeCurrentTabFromRepository() } } + override fun closeAndSelectSourceTab() { + GlobalScope.launch { removeAndSelectTabsFromRepository() } + } + + private suspend fun removeAndSelectTabsFromRepository() { + val currentTab = tabRepository.liveSelectedTab.value + currentTab?.let { + tabRepository.deleteAndSelectSource(currentTab) + } + } + fun onUserPressedForward() { navigationAwareLoginDetector.onEvent(NavigationEvent.UserAction.NavigateForward) if (!currentBrowserViewState().browserShowing) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 6b80b16988a9..0e2f93c0474b 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -120,11 +120,19 @@ class BrowserViewModel( } suspend fun onNewTabRequested(isDefaultTab: Boolean = false): String { - return tabRepository.add(isDefaultTab = isDefaultTab) + return tabRepository.addWithSource( + isDefaultTab = isDefaultTab, + source = tabRepository.liveSelectedTab.value + ) } suspend fun onOpenInNewTabRequested(query: String, skipHome: Boolean = false): String { - return tabRepository.add(queryUrlConverter.convertQueryToUrl(query), skipHome, isDefaultTab = false) + return tabRepository.addWithSource( + queryUrlConverter.convertQueryToUrl(query), + skipHome, + isDefaultTab = false, + source = tabRepository.liveSelectedTab.value + ) } suspend fun onTabsUpdated(tabs: List?) { diff --git a/app/src/main/java/com/duckduckgo/app/browser/DownloadConfirmationFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/DownloadConfirmationFragment.kt index fd26a19d98e4..db17eb8b5b8e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/DownloadConfirmationFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/DownloadConfirmationFragment.kt @@ -49,7 +49,7 @@ class DownloadConfirmationFragment : BottomSheetDialogFragment() { lateinit var downloadListener: FileDownloadListener private val pendingDownload: PendingFileDownload by lazy { - arguments!![PENDING_DOWNLOAD_BUNDLE_KEY] as PendingFileDownload + requireArguments()[PENDING_DOWNLOAD_BUNDLE_KEY] as PendingFileDownload } private var file: File? = null @@ -87,6 +87,7 @@ class DownloadConfirmationFragment : BottomSheetDialogFragment() { } view.cancel.setOnClickListener { Timber.i("Cancelled download for url ${pendingDownload.url}") + downloadListener.downloadCancelled() dismiss() } @@ -126,6 +127,7 @@ class DownloadConfirmationFragment : BottomSheetDialogFragment() { Timber.e("No suitable activity found") Toast.makeText(activity, R.string.downloadConfirmationUnableToOpenFileText, Toast.LENGTH_SHORT).show() } + downloadListener.downloadOpened() } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt index 00d872c40b94..15859a2161ab 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt @@ -47,6 +47,7 @@ interface WebViewClientListener { fun recoverFromRenderProcessGone() fun requiresAuthentication(request: BasicAuthenticationRequest) fun closeCurrentTab() + fun closeAndSelectSourceTab() fun upgradedToHttps() fun surrogateDetected(surrogate: SurrogateResponse) diff --git a/app/src/main/java/com/duckduckgo/app/browser/downloader/FileDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/downloader/FileDownloader.kt index 2473b1b6beb3..826ed1340be5 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/downloader/FileDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/downloader/FileDownloader.kt @@ -52,6 +52,8 @@ class FileDownloader @Inject constructor( fun downloadStarted() fun downloadFinished(file: File, mimeType: String?) fun downloadFailed(message: String, downloadFailReason: DownloadFailReason) + fun downloadCancelled() + fun downloadOpened() } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloader.kt b/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloader.kt index a976d0c039a4..08e1c732a445 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloader.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/downloader/NetworkFileDownloader.kt @@ -46,6 +46,7 @@ class NetworkFileDownloader @Inject constructor(private val context: Context) { } val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager? manager?.enqueue(request) + callback.downloadStarted() } private fun downloadManagerAvailable(): Boolean { 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 cf634d945d9a..95d760fe3879 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 @@ -58,7 +58,7 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.usage.search.SearchCountEntity @Database( - exportSchema = true, version = 21, entities = [ + exportSchema = true, version = 22, entities = [ TdsTracker::class, TdsEntity::class, TdsDomainEntity::class, @@ -292,6 +292,12 @@ class MigrationsProvider(val context: Context) { } } + val MIGRATION_21_TO_22: Migration = object : Migration(21, 22) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE `tabs` ADD COLUMN `sourceTabId` TEXT") + } + } + val ALL_MIGRATIONS: List get() = listOf( MIGRATION_1_TO_2, @@ -313,7 +319,8 @@ class MigrationsProvider(val context: Context) { MIGRATION_17_TO_18, MIGRATION_18_TO_19, MIGRATION_19_TO_20, - MIGRATION_20_TO_21 + MIGRATION_20_TO_21, + MIGRATION_21_TO_22 ) @Deprecated( diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index e70c014fae72..bfd821e73ef9 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -51,6 +51,18 @@ class TabDataRepository @Inject constructor( return tabId } + override suspend fun addWithSource(url: String?, skipHome: Boolean, isDefaultTab: Boolean, source: TabEntity?): String { + val tabId = generateTabId() + add( + tabId, + buildSiteData(url), + skipHome = skipHome, + isDefaultTab = isDefaultTab, + source = source + ) + return tabId + } + private fun generateTabId() = UUID.randomUUID().toString() private fun buildSiteData(url: String?): MutableLiveData { @@ -62,7 +74,7 @@ class TabDataRepository @Inject constructor( return data } - override suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean, isDefaultTab: Boolean) { + override suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean, isDefaultTab: Boolean, source: TabEntity?) { siteData[tabId] = data databaseExecutor().scheduleDirect { @@ -81,7 +93,7 @@ class TabDataRepository @Inject constructor( } Timber.i("About to add a new tab, isDefaultTab: $isDefaultTab. $tabId, position: $position") - tabsDao.addAndSelectTab(TabEntity(tabId, data.value?.url, data.value?.title, skipHome, true, position)) + tabsDao.addAndSelectTab(TabEntity(tabId, data.value?.url, data.value?.title, skipHome, true, position, null, source?.tabId)) } } @@ -128,6 +140,18 @@ class TabDataRepository @Inject constructor( siteData.remove(tab.tabId) } + override suspend fun deleteAndSelectSource(tabToDelete: TabEntity) { + databaseExecutor().scheduleDirect { + deleteOldPreviewImages(tabToDelete.tabId) + tabToDelete.sourceTabId?.let { sourceTabId -> + if (tabsDao.tab(sourceTabId) != null) { + tabsDao.insertTabSelection(TabSelectionEntity(tabId = sourceTabId)) + tabsDao.deleteTab(tabToDelete) + } + } + } + } + override fun deleteAll() { Timber.i("Deleting tabs right now") tabsDao.deleteAllTabs() diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabEntitiy.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabEntitiy.kt index bff04a980a71..4a4cd53958be 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabEntitiy.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabEntitiy.kt @@ -33,7 +33,8 @@ data class TabEntity( var skipHome: Boolean = false, var viewed: Boolean = true, var position: Int, - var tabPreviewFile: String? = null + var tabPreviewFile: String? = null, + var sourceTabId: String? = null ) val TabEntity.isBlank: Boolean diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt index ad7e2067d4ec..37711d5689ce 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt @@ -31,7 +31,9 @@ interface TabRepository { */ suspend fun add(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false): String - suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean = false, isDefaultTab: Boolean = false) + suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean = false, isDefaultTab: Boolean = false, source: TabEntity? = null) + + suspend fun addWithSource(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false, source: TabEntity?): String suspend fun addNewTabAfterExistingTab(url: String? = null, tabId: String) @@ -44,6 +46,8 @@ interface TabRepository { suspend fun delete(tab: TabEntity) + suspend fun deleteAndSelectSource(tabToDelete: TabEntity) + fun deleteAll() suspend fun select(tabId: String) From d8aa2680abf4f82c4848fcf0a331d9ba336b87f3 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 4 Jul 2020 02:22:39 -0400 Subject: [PATCH 2/9] Removes need to pass entire TabEntity in favor of just passing sourceTabId --- .../java/com/duckduckgo/app/browser/BrowserViewModel.kt | 4 ++-- .../com/duckduckgo/app/tabs/model/TabDataRepository.kt | 8 ++++---- .../java/com/duckduckgo/app/tabs/model/TabRepository.kt | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 0e2f93c0474b..4728a4eb7dfd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -122,7 +122,7 @@ class BrowserViewModel( suspend fun onNewTabRequested(isDefaultTab: Boolean = false): String { return tabRepository.addWithSource( isDefaultTab = isDefaultTab, - source = tabRepository.liveSelectedTab.value + sourceTabId = tabRepository.liveSelectedTab.value?.tabId ) } @@ -131,7 +131,7 @@ class BrowserViewModel( queryUrlConverter.convertQueryToUrl(query), skipHome, isDefaultTab = false, - source = tabRepository.liveSelectedTab.value + sourceTabId = tabRepository.liveSelectedTab.value?.tabId ) } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index bfd821e73ef9..4f6b5ea6e58c 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -51,14 +51,14 @@ class TabDataRepository @Inject constructor( return tabId } - override suspend fun addWithSource(url: String?, skipHome: Boolean, isDefaultTab: Boolean, source: TabEntity?): String { + override suspend fun addWithSource(url: String?, skipHome: Boolean, isDefaultTab: Boolean, sourceTabId: String?): String { val tabId = generateTabId() add( tabId, buildSiteData(url), skipHome = skipHome, isDefaultTab = isDefaultTab, - source = source + sourceTabId = sourceTabId ) return tabId } @@ -74,7 +74,7 @@ class TabDataRepository @Inject constructor( return data } - override suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean, isDefaultTab: Boolean, source: TabEntity?) { + override suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean, isDefaultTab: Boolean, sourceTabId: String?) { siteData[tabId] = data databaseExecutor().scheduleDirect { @@ -93,7 +93,7 @@ class TabDataRepository @Inject constructor( } Timber.i("About to add a new tab, isDefaultTab: $isDefaultTab. $tabId, position: $position") - tabsDao.addAndSelectTab(TabEntity(tabId, data.value?.url, data.value?.title, skipHome, true, position, null, source?.tabId)) + tabsDao.addAndSelectTab(TabEntity(tabId, data.value?.url, data.value?.title, skipHome, true, position, null, sourceTabId)) } } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt index 37711d5689ce..f6738795dc79 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt @@ -31,9 +31,9 @@ interface TabRepository { */ suspend fun add(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false): String - suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean = false, isDefaultTab: Boolean = false, source: TabEntity? = null) + suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean = false, isDefaultTab: Boolean = false, sourceTabId: String? = null) - suspend fun addWithSource(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false, source: TabEntity?): String + suspend fun addWithSource(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false, sourceTabId: String?): String suspend fun addNewTabAfterExistingTab(url: String? = null, tabId: String) From cdd5a1c152c7a1fd785d9a0209094f4225813aec Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 4 Jul 2020 12:20:21 -0400 Subject: [PATCH 3/9] Code clean up, scope fix, and mirrors TabDataRepo delete closer in deleteAndSelectSource --- .../app/browser/BrowserTabFragment.kt | 22 +++++++++---------- .../app/browser/BrowserTabViewModel.kt | 2 +- .../app/tabs/model/TabDataRepository.kt | 4 +++- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index c4c252a56355..1f179d43582a 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1090,9 +1090,13 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } } - private fun closeAndReturnToSource() { - launch { - viewModel.closeAndSelectSourceTab() + private fun closeAndReturnToSourceIfBlankTab() { + if (viewModel.url == null) { + launch { + dismissDownloadFragment() + destroyWebView() + viewModel.closeAndSelectSourceTab() + } } } @@ -1100,9 +1104,7 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi return object : FileDownloadListener { override fun downloadStarted() { fileDownloadNotificationManager.showDownloadInProgressNotification() - if (viewModel.url == null) { - closeAndReturnToSource() - } + closeAndReturnToSourceIfBlankTab() } override fun downloadFinished(file: File, mimeType: String?) { @@ -1127,15 +1129,11 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi } override fun downloadCancelled() { - if (viewModel.url == null) { - closeAndReturnToSource() - } + closeAndReturnToSourceIfBlankTab() } override fun downloadOpened() { - if (viewModel.url == null) { - closeAndReturnToSource() - } + closeAndReturnToSourceIfBlankTab() } private fun showDownloadManagerAppSettings() { 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 9bad69e581e0..e2380c8a1210 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -482,7 +482,7 @@ class BrowserTabViewModel( } override fun closeAndSelectSourceTab() { - GlobalScope.launch { removeAndSelectTabsFromRepository() } + viewModelScope.launch { removeAndSelectTabsFromRepository() } } private suspend fun removeAndSelectTabsFromRepository() { diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index 4f6b5ea6e58c..6e4922766849 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -143,13 +143,15 @@ class TabDataRepository @Inject constructor( override suspend fun deleteAndSelectSource(tabToDelete: TabEntity) { databaseExecutor().scheduleDirect { deleteOldPreviewImages(tabToDelete.tabId) + tabToDelete.sourceTabId?.let { sourceTabId -> if (tabsDao.tab(sourceTabId) != null) { - tabsDao.insertTabSelection(TabSelectionEntity(tabId = sourceTabId)) tabsDao.deleteTab(tabToDelete) + tabsDao.insertTabSelection(TabSelectionEntity(tabId = sourceTabId)) } } } + siteData.remove(tabToDelete.tabId) } override fun deleteAll() { From 9001def861557c7dcf7c89a41aca9ee64e36ad17 Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 4 Jul 2020 20:26:04 -0400 Subject: [PATCH 4/9] Updates tests to expected addWithSource rather than just add for new tabs --- .../23.json | 752 ++++++++++++++++++ .../app/browser/BrowserViewModelTest.kt | 6 +- .../app/tabs/model/TabRepository.kt | 2 +- 3 files changed, 757 insertions(+), 3 deletions(-) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/23.json diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/23.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/23.json new file mode 100644 index 000000000000..3ed85e766b0a --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/23.json @@ -0,0 +1,752 @@ +{ + "formatVersion": 1, + "database": { + "version": 23, + "identityHash": "03673f6d5937091013955e2520b94c0e", + "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, `sourceTabId` 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 + }, + { + "fieldPath": "sourceTabId", + "columnName": "sourceTabId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "tabId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tabs_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `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 IF NOT EXISTS `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, `timestamp` INTEGER NOT NULL, `version` 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 + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "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": [] + }, + { + "tableName": "fireproofWebsites", + "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_events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "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, '03673f6d5937091013955e2520b94c0e')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index ced386fb72e5..59991b4b67d1 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -121,16 +121,18 @@ class BrowserViewModelTest { @Test fun whenNewTabRequestedThenTabAddedToRepository() = runBlocking { + whenever(mockTabRepository.liveSelectedTab).doReturn(MutableLiveData()) testee.onNewTabRequested() - verify(mockTabRepository).add() + verify(mockTabRepository).addWithSource() } @Test fun whenOpenInNewTabRequestedThenTabAddedToRepository() = runBlocking { val url = "http://example.com" whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) + whenever(mockTabRepository.liveSelectedTab).doReturn(MutableLiveData()) testee.onOpenInNewTabRequested(url) - verify(mockTabRepository).add(url) + verify(mockTabRepository).addWithSource(url) } @Test diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt index 603d0af0d4b9..e510c6f5ecb1 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt @@ -33,7 +33,7 @@ interface TabRepository { suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean = false, isDefaultTab: Boolean = false, sourceTabId: String? = null) - suspend fun addWithSource(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false, sourceTabId: String?): String + suspend fun addWithSource(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false, sourceTabId: String? = null): String suspend fun addNewTabAfterExistingTab(url: String? = null, tabId: String) From 2c2488509084d316c86cc6e810a6eb80eb6028f5 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 28 Jul 2020 18:53:50 -0500 Subject: [PATCH 5/9] Addresses PR feedback primarily letting fragment lifecycle handle cleanup and referencing the liveSelectedTab from inside the repo rather than duplicating the value and passing as a param --- .../app/browser/BrowserTabFragment.kt | 2 -- .../app/browser/BrowserTabViewModel.kt | 9 +++---- .../app/browser/BrowserViewModel.kt | 8 ++---- .../app/tabs/model/TabDataRepository.kt | 25 +++++++++++-------- .../app/tabs/model/TabRepository.kt | 4 +-- 5 files changed, 21 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index c22862cc230d..3ffc8ec25c64 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -1090,8 +1090,6 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi private fun closeAndReturnToSourceIfBlankTab() { if (viewModel.url == null) { launch { - dismissDownloadFragment() - destroyWebView() viewModel.closeAndSelectSourceTab() } } 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 c9780203a179..4bcf0b7f6dcb 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -506,14 +506,11 @@ class BrowserTabViewModel( } override fun closeAndSelectSourceTab() { - viewModelScope.launch { removeAndSelectTabsFromRepository() } + viewModelScope.launch { removeAndSelectTabFromRepository() } } - private suspend fun removeAndSelectTabsFromRepository() { - val currentTab = tabRepository.liveSelectedTab.value - currentTab?.let { - tabRepository.deleteAndSelectSource(currentTab) - } + private suspend fun removeAndSelectTabFromRepository() { + tabRepository.deleteCurrentTabAndSelectSource() } fun onUserPressedForward() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index 50e3e9b9009f..59b1ccdb1398 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -129,18 +129,14 @@ class BrowserViewModel( } suspend fun onNewTabRequested(isDefaultTab: Boolean = false): String { - return tabRepository.addWithSource( - isDefaultTab = isDefaultTab, - sourceTabId = tabRepository.liveSelectedTab.value?.tabId - ) + return tabRepository.addWithSource(isDefaultTab = isDefaultTab) } suspend fun onOpenInNewTabRequested(query: String, skipHome: Boolean = false): String { return tabRepository.addWithSource( queryUrlConverter.convertQueryToUrl(query), skipHome, - isDefaultTab = false, - sourceTabId = tabRepository.liveSelectedTab.value?.tabId + isDefaultTab = false ) } diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index 658725d8bf2e..e84ce11cb007 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -51,14 +51,14 @@ class TabDataRepository @Inject constructor( return tabId } - override suspend fun addWithSource(url: String?, skipHome: Boolean, isDefaultTab: Boolean, sourceTabId: String?): String { + override suspend fun addWithSource(url: String?, skipHome: Boolean, isDefaultTab: Boolean): String { val tabId = generateTabId() add( - tabId, - buildSiteData(url), - skipHome = skipHome, - isDefaultTab = isDefaultTab, - sourceTabId = sourceTabId + tabId, + buildSiteData(url), + skipHome = skipHome, + isDefaultTab = isDefaultTab, + sourceTabId = liveSelectedTab.value?.tabId ) return tabId } @@ -149,18 +149,21 @@ class TabDataRepository @Inject constructor( siteData.remove(tab.tabId) } - override suspend fun deleteAndSelectSource(tabToDelete: TabEntity) { + override suspend fun deleteCurrentTabAndSelectSource() { + val tabToDelete = liveSelectedTab.value databaseExecutor().scheduleDirect { - deleteOldPreviewImages(tabToDelete.tabId) + tabToDelete?.tabId?.let { + deleteOldPreviewImages(tabToDelete.tabId) + tabsDao.deleteTab(tabToDelete) + siteData.remove(tabToDelete.tabId) + } - tabToDelete.sourceTabId?.let { sourceTabId -> + tabToDelete?.sourceTabId?.let { sourceTabId -> if (tabsDao.tab(sourceTabId) != null) { - tabsDao.deleteTab(tabToDelete) tabsDao.insertTabSelection(TabSelectionEntity(tabId = sourceTabId)) } } } - siteData.remove(tabToDelete.tabId) } override fun deleteAll() { diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt index e510c6f5ecb1..84b87a3941c3 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt @@ -33,7 +33,7 @@ interface TabRepository { suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean = false, isDefaultTab: Boolean = false, sourceTabId: String? = null) - suspend fun addWithSource(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false, sourceTabId: String? = null): String + suspend fun addWithSource(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false): String suspend fun addNewTabAfterExistingTab(url: String? = null, tabId: String) @@ -46,7 +46,7 @@ interface TabRepository { suspend fun delete(tab: TabEntity) - suspend fun deleteAndSelectSource(tabToDelete: TabEntity) + suspend fun deleteCurrentTabAndSelectSource() fun deleteAll() From d4fb51a6677388f57df688f94cb3814c3fad8e69 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 30 Jul 2020 20:40:45 -0500 Subject: [PATCH 6/9] Addresses PR feedback - info related to the source tab will be pulled directly from the tabsDao rather than the LiveData object. --- .../app/tabs/model/TabDataRepository.kt | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index e84ce11cb007..bf1bfc69c719 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -25,8 +25,10 @@ import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.tabs.db.TabsDao import io.reactivex.Scheduler import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.* import javax.inject.Inject @@ -53,13 +55,20 @@ class TabDataRepository @Inject constructor( override suspend fun addWithSource(url: String?, skipHome: Boolean, isDefaultTab: Boolean): String { val tabId = generateTabId() + var sourceTabId: String? = null + + withContext(Dispatchers.IO) { + sourceTabId = tabsDao.selectedTab()?.tabId + } + add( - tabId, - buildSiteData(url), - skipHome = skipHome, - isDefaultTab = isDefaultTab, - sourceTabId = liveSelectedTab.value?.tabId + tabId, + buildSiteData(url), + skipHome = skipHome, + isDefaultTab = isDefaultTab, + sourceTabId = sourceTabId ) + return tabId } @@ -150,8 +159,8 @@ class TabDataRepository @Inject constructor( } override suspend fun deleteCurrentTabAndSelectSource() { - val tabToDelete = liveSelectedTab.value databaseExecutor().scheduleDirect { + val tabToDelete = tabsDao.selectedTab() tabToDelete?.tabId?.let { deleteOldPreviewImages(tabToDelete.tabId) tabsDao.deleteTab(tabToDelete) From edb15929323484b0c27d3f1e002738e39e753064 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 31 Jul 2020 22:48:10 -0500 Subject: [PATCH 7/9] Addresses PR feedback - Create and use transaction rather than separate db calls to ensure data integrity. --- .../com/duckduckgo/app/tabs/db/TabsDao.kt | 18 +++++++ .../app/tabs/model/TabDataRepository.kt | 48 ++++++++++--------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt b/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt index 04a8bc29d419..5d0dfa122d02 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/db/TabsDao.kt @@ -85,6 +85,24 @@ abstract class TabsDao { } } + @Transaction + open fun deleteTabAndUpdateSelection(tab: TabEntity, newSelectedTab: TabEntity? = null) { + deleteTab(tab) + + if (newSelectedTab != null) { + insertTabSelection(TabSelectionEntity(tabId = newSelectedTab.tabId)) + return + } + + if (selectedTab() != null) { + return + } + + firstTab()?.let { + insertTabSelection(TabSelectionEntity(tabId = it.tabId)) + } + } + @Insert(onConflict = OnConflictStrategy.REPLACE) abstract fun insertTabSelection(tabSelectionEntity: TabSelectionEntity) diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index bf1bfc69c719..66ae59963cd5 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -55,18 +55,16 @@ class TabDataRepository @Inject constructor( override suspend fun addWithSource(url: String?, skipHome: Boolean, isDefaultTab: Boolean): String { val tabId = generateTabId() - var sourceTabId: String? = null - - withContext(Dispatchers.IO) { - sourceTabId = tabsDao.selectedTab()?.tabId + val sourceTabId = withContext(Dispatchers.IO) { + tabsDao.selectedTab()?.tabId } add( - tabId, - buildSiteData(url), - skipHome = skipHome, - isDefaultTab = isDefaultTab, - sourceTabId = sourceTabId + tabId, + buildSiteData(url), + skipHome = skipHome, + isDefaultTab = isDefaultTab, + sourceTabId = sourceTabId ) return tabId @@ -102,7 +100,15 @@ class TabDataRepository @Inject constructor( } Timber.i("About to add a new tab, isDefaultTab: $isDefaultTab. $tabId, position: $position") - tabsDao.addAndSelectTab(TabEntity(tabId, data.value?.url, data.value?.title, skipHome, true, position, null, sourceTabId)) + tabsDao.addAndSelectTab(TabEntity( + tabId = tabId, + url = data.value?.url, + title = data.value?.title, + skipHome = skipHome, + viewed = true, + position = position, + sourceTabId = sourceTabId + )) } } @@ -160,18 +166,16 @@ class TabDataRepository @Inject constructor( override suspend fun deleteCurrentTabAndSelectSource() { databaseExecutor().scheduleDirect { - val tabToDelete = tabsDao.selectedTab() - tabToDelete?.tabId?.let { - deleteOldPreviewImages(tabToDelete.tabId) - tabsDao.deleteTab(tabToDelete) - siteData.remove(tabToDelete.tabId) - } - - tabToDelete?.sourceTabId?.let { sourceTabId -> - if (tabsDao.tab(sourceTabId) != null) { - tabsDao.insertTabSelection(TabSelectionEntity(tabId = sourceTabId)) - } - } + val tabToDelete = tabsDao.selectedTab() ?: return@scheduleDirect + + deleteOldPreviewImages(tabToDelete.tabId) + val tabToSelect = tabToDelete.sourceTabId + .takeUnless { it.isNullOrBlank() } + ?.let { + tabsDao.tab(it) + } + tabsDao.deleteTabAndUpdateSelection(tabToDelete, tabToSelect) + siteData.remove(tabToDelete.tabId) } } From 702a54cfb49c4305d5258c135e604393634bb9a8 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 31 Jul 2020 22:48:55 -0500 Subject: [PATCH 8/9] Adds tests for the 2 new TabDataRepo methods (AddWithSource and DeleteCurrentTabAndSelectSource) --- .../app/tabs/model/TabDataRepositoryTest.kt | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt index 2807e5d9583d..83b2757bdeff 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt @@ -264,6 +264,39 @@ class TabDataRepositoryTest { db.close() } + @Test + fun whenAddWithSourceEnsureTabEntryContainsExpectedSourceId() = runBlocking { + val db = createDatabase() + val dao = db.tabsDao() + val sourceTab = TabEntity(tabId = "sourceId", url = "http://www.example.com", position = 0) + dao.addAndSelectTab(sourceTab) + + testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister) + + val addedTabId = testee.addWithSource("http://www.example.com", skipHome = false, isDefaultTab = false) + val addedTab = testee.liveSelectedTab.blockingObserve() + assertEquals(addedTabId, addedTab?.tabId) + assertEquals(addedTab?.sourceTabId, sourceTab.tabId) + } + + @Test + fun whenDeleteCurrentTabAndSelectSourceLiveSelectedTabReturnsToSourceTab() = runBlocking { + val db = createDatabase() + val dao = db.tabsDao() + val sourceTab = TabEntity(tabId = "sourceId", url = "http://www.example.com", position = 0) + val tabToDelete = TabEntity(tabId = "tabToDeleteId", url = "http://www.example.com", position = 1, sourceTabId = "sourceId") + dao.addAndSelectTab(sourceTab) + dao.addAndSelectTab(tabToDelete) + + testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister) + + var currentSelectedTabId = testee.liveSelectedTab.blockingObserve()?.tabId + assertEquals(currentSelectedTabId, tabToDelete.tabId) + testee.deleteCurrentTabAndSelectSource() + currentSelectedTabId = testee.liveSelectedTab.blockingObserve()?.tabId + assertEquals(currentSelectedTabId, sourceTab.tabId) + } + private fun createDatabase(): AppDatabase { return Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().targetContext, AppDatabase::class.java) .allowMainThreadQueries() From e2f9f0c09ca41bb8dd98014485c4f677a65f1ca1 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 3 Aug 2020 13:15:55 -0500 Subject: [PATCH 9/9] updates db migrations and tests after merge --- .../24.json | 752 ++++++++++++++++++ .../app/global/db/AppDatabaseTest.kt | 4 +- .../app/tabs/model/TabDataRepositoryTest.kt | 4 +- .../duckduckgo/app/global/db/AppDatabase.kt | 7 +- 4 files changed, 760 insertions(+), 7 deletions(-) create mode 100644 app/schemas/com.duckduckgo.app.global.db.AppDatabase/24.json diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/24.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/24.json new file mode 100644 index 000000000000..bd4a3afcb987 --- /dev/null +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/24.json @@ -0,0 +1,752 @@ +{ + "formatVersion": 1, + "database": { + "version": 24, + "identityHash": "03673f6d5937091013955e2520b94c0e", + "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, `sourceTabId` 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 + }, + { + "fieldPath": "sourceTabId", + "columnName": "sourceTabId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "tabId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tabs_tabId", + "unique": false, + "columnNames": [ + "tabId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `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 IF NOT EXISTS `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, `timestamp` INTEGER NOT NULL, `version` 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 + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "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": [] + }, + { + "tableName": "fireproofWebsites", + "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_events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "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, '03673f6d5937091013955e2520b94c0e')" + ] + } +} \ No newline at end of file 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 ace48a505edf..2df96ee7e186 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 @@ -282,8 +282,8 @@ class AppDatabaseTest { } @Test - fun whenMigratingFromVersion22To23ThenValidationSucceeds() { - createDatabaseAndMigrate(22, 23, migrationsProvider.MIGRATION_22_TO_23) + fun whenMigratingFromVersion23To24ThenValidationSucceeds() { + createDatabaseAndMigrate(23, 24, migrationsProvider.MIGRATION_23_TO_24) } private fun createDatabase(version: Int) { diff --git a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt index 47549f0fa1cf..64123aab20b9 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt @@ -304,7 +304,7 @@ class TabDataRepositoryTest { val sourceTab = TabEntity(tabId = "sourceId", url = "http://www.example.com", position = 0) dao.addAndSelectTab(sourceTab) - testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister) + testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister, useOurAppDetector) val addedTabId = testee.addWithSource("http://www.example.com", skipHome = false, isDefaultTab = false) val addedTab = testee.liveSelectedTab.blockingObserve() @@ -321,7 +321,7 @@ class TabDataRepositoryTest { dao.addAndSelectTab(sourceTab) dao.addAndSelectTab(tabToDelete) - testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister) + testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister, useOurAppDetector) var currentSelectedTabId = testee.liveSelectedTab.blockingObserve()?.tabId assertEquals(currentSelectedTabId, tabToDelete.tabId) 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 31524467157d..ea4e529ffab0 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 @@ -63,7 +63,7 @@ import com.duckduckgo.app.usage.search.SearchCountDao import com.duckduckgo.app.usage.search.SearchCountEntity @Database( - exportSchema = true, version = 23, entities = [ + exportSchema = true, version = 24, entities = [ TdsTracker::class, TdsEntity::class, TdsDomainEntity::class, @@ -316,7 +316,7 @@ class MigrationsProvider( } } - val MIGRATION_22_TO_23: Migration = object : Migration(22, 23) { + val MIGRATION_23_TO_24: Migration = object : Migration(23, 24) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE `tabs` ADD COLUMN `sourceTabId` TEXT") } @@ -345,7 +345,8 @@ class MigrationsProvider( MIGRATION_19_TO_20, MIGRATION_20_TO_21, MIGRATION_21_TO_22, - MIGRATION_22_TO_23 + MIGRATION_22_TO_23, + MIGRATION_23_TO_24 ) @Deprecated(