diff --git a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/24.json b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/24.json index bd4a3afcb987..3fef6ddc1ee2 100644 --- a/app/schemas/com.duckduckgo.app.global.db.AppDatabase/24.json +++ b/app/schemas/com.duckduckgo.app.global.db.AppDatabase/24.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 24, - "identityHash": "03673f6d5937091013955e2520b94c0e", + "identityHash": "d6df0e21b463f404e4ea0a430f7d905c", "entities": [ { "tableName": "tds_tracker", @@ -258,7 +258,7 @@ }, { "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`))", + "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`), FOREIGN KEY(`sourceTabId`) REFERENCES `tabs`(`tabId`) ON UPDATE SET NULL ON DELETE SET NULL )", "fields": [ { "fieldPath": "tabId", @@ -325,7 +325,19 @@ "createSql": "CREATE INDEX IF NOT EXISTS `index_tabs_tabId` ON `${TABLE_NAME}` (`tabId`)" } ], - "foreignKeys": [] + "foreignKeys": [ + { + "table": "tabs", + "onDelete": "SET NULL", + "onUpdate": "SET NULL", + "columns": [ + "sourceTabId" + ], + "referencedColumns": [ + "tabId" + ] + } + ] }, { "tableName": "tab_selection", @@ -746,7 +758,7 @@ "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')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6df0e21b463f404e4ea0a430f7d905c')" ] } } \ 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 ba957ba2ac48..bd2fa8b65752 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -476,7 +476,9 @@ class BrowserTabViewModelTest { givenOneActiveTabSelected() givenInvalidatedGlobalLayout() testee.onUserSubmittedQuery("foo") - assertCommandIssued() + assertCommandIssued { + assertNull(sourceTabId) + } } @Test @@ -1093,7 +1095,9 @@ class BrowserTabViewModelTest { testee.onRefreshRequested() - assertCommandIssued() + assertCommandIssued() { + assertNull(sourceTabId) + } } @Test @@ -1188,6 +1192,10 @@ class BrowserTabViewModelTest { testee.userSelectedItemFromLongPressMenu(longPressTarget, mockMenItem) val command = captureCommands().value as Command.OpenInNewTab assertEquals("http://example.com", command.query) + + assertCommandIssued { + assertNotNull(sourceTabId) + } } @Test @@ -1319,18 +1327,6 @@ class BrowserTabViewModelTest { assertEquals(true, testee.browserViewState.value?.browserShowing) } - @Test - fun whenOpenInNewTabThenOpenInNewTabCommandWithCorrectUrlSent() { - val url = "https://example.com" - testee.openInNewTab(url) - verify(mockCommandObserver).onChanged(commandCaptor.capture()) - - val command = commandCaptor.lastValue - assertTrue(command is Command.OpenInNewTab) - command as Command.OpenInNewTab - assertEquals(url, command.query) - } - @Test fun whenRecoveringFromProcessGoneThenShowErrorWithAction() { testee.recoverFromRenderProcessGone() @@ -1348,6 +1344,7 @@ class BrowserTabViewModelTest { assertCommandIssued { assertEquals("https://example.com", query) + assertNull(sourceTabId) } } @@ -1558,6 +1555,16 @@ class BrowserTabViewModelTest { assertTrue(commandCaptor.allValues.contains(Command.ShowKeyboard)) } + @Test + fun whenUserPressesBackOnATabWithASourceTabThenDeleteCurrentAndSelectSource() = coroutineRule.runBlocking { + selectedTabLiveData.value = TabEntity("TAB_ID", "https://example.com", position = 0, sourceTabId = "TAB_ID_SOURCE") + setupNavigation(isBrowsing = true) + + testee.onUserPressedBack() + + verify(mockTabsRepository).deleteCurrentTabAndSelectSource() + } + @Test fun whenScheduledSurveyChangesAndInstalledDaysMatchThenCtaIsSurvey() { testee.onSurveyChanged(Survey("abc", "http://example.com", daysInstalled = 1, status = Survey.Status.SCHEDULED)) 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 5eae0cced112..a1dcda909021 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -123,7 +123,14 @@ class BrowserViewModelTest { fun whenNewTabRequestedThenTabAddedToRepository() = runBlocking { whenever(mockTabRepository.liveSelectedTab).doReturn(MutableLiveData()) testee.onNewTabRequested() - verify(mockTabRepository).addWithSource() + verify(mockTabRepository).add() + } + + @Test + fun whenNewTabRequestedFromSourceTabThenTabAddedToRepositoryWithSourceTabId() = runBlocking { + whenever(mockTabRepository.liveSelectedTab).doReturn(MutableLiveData()) + testee.onNewTabRequested("sourceTabId") + verify(mockTabRepository).addFromSourceTab(sourceTabId = "sourceTabId") } @Test @@ -132,13 +139,22 @@ class BrowserViewModelTest { whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) whenever(mockTabRepository.liveSelectedTab).doReturn(MutableLiveData()) testee.onOpenInNewTabRequested(url) - verify(mockTabRepository).addWithSource(url) + verify(mockTabRepository).add(url = url, skipHome = false) + } + + @Test + fun whenOpenInNewTabRequestedWithSourceTabIdThenTabAddedToRepositoryWithSourceTabId() = runBlocking { + val url = "http://example.com" + whenever(mockOmnibarEntryConverter.convertQueryToUrl(url)).thenReturn(url) + whenever(mockTabRepository.liveSelectedTab).doReturn(MutableLiveData()) + testee.onOpenInNewTabRequested(url, sourceTabId = "tabId") + verify(mockTabRepository).addFromSourceTab(url = url, skipHome = false, sourceTabId = "tabId") } @Test - fun whenTabsUpdatedAndNoTabsThenNewTabAddedToRepository() = runBlocking { + fun whenTabsUpdatedAndNoTabsThenDefaultTabAddedToRepository() = runBlocking { testee.onTabsUpdated(ArrayList()) - verify(mockTabRepository).add(null, false, true) + verify(mockTabRepository).addDefaultTab() } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt b/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt index 26b056ea0e08..751da71f4bae 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/tabs/db/TabsDaoTest.kt @@ -206,7 +206,6 @@ class TabsDaoTest { @Test fun whenTabInsertedAtPositionThenOtherTabsReordered() { - testee.insertTab(TabEntity("TAB_ID1", position = 0)) testee.insertTab(TabEntity("TAB_ID2", position = 1)) testee.insertTab(TabEntity("TAB_ID3", position = 2)) @@ -226,7 +225,18 @@ class TabsDaoTest { assertEquals(3, tabs[3].position) assertEquals("TAB_ID3", tabs[3].tabId) - } + @Test + fun whenSourceTabDeletedThenRelatedTabsUpdated() { + val firstTab = TabEntity("TAB_ID", "http//updatedexample.com", position = 0) + val secondTab = TabEntity("TAB_ID_1", "http//updatedexample.com", position = 1, sourceTabId = "TAB_ID") + testee.insertTab(firstTab) + testee.insertTab(secondTab) + + testee.deleteTab(firstTab) + + assertNotNull(testee.tab("TAB_ID_1")) + assertNull(testee.tab("TAB_ID_1")?.sourceTabId) + } } 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 64123aab20b9..56f0caa1c055 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 @@ -19,7 +19,6 @@ package com.duckduckgo.app.tabs.model import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.MutableLiveData import androidx.room.Room import androidx.test.annotation.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry @@ -29,20 +28,13 @@ import com.duckduckgo.app.blockingObserve import com.duckduckgo.app.browser.tabpreview.WebViewPreviewPersister import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.events.db.UserEventsStore -import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.SiteFactory import com.duckduckgo.app.global.useourapp.UseOurAppDetector import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_DOMAIN import com.duckduckgo.app.privacy.model.PrivacyPractices import com.duckduckgo.app.tabs.db.TabsDao import com.duckduckgo.app.trackerdetection.EntityLookup -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.argumentCaptor -import com.nhaarman.mockitokotlin2.eq -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify -import com.nhaarman.mockitokotlin2.whenever +import com.nhaarman.mockitokotlin2.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import org.junit.Assert.* @@ -94,7 +86,6 @@ class TabDataRepositoryTest { @Test fun whenAddNewTabAfterExistingTabWithUrlWithNoHostThenUsesUrlAsTitle() = runBlocking { - val badUrl = "//bad/url" testee.addNewTabAfterExistingTab(badUrl, "tabid") val captor = argumentCaptor() @@ -163,21 +154,6 @@ class TabDataRepositoryTest { assertEquals(url, testee.retrieveSiteData(createdId).value!!.url) } - @Test - fun whenAddRecordCalledThenTabAddedAndSiteDataAdded() = runBlocking { - val record = MutableLiveData() - testee.add(TAB_ID, record) - verify(mockDao).addAndSelectTab(any()) - assertSame(record, testee.retrieveSiteData(TAB_ID)) - } - - @Test - fun whenDataExistsForTabThenRetrieveReturnsIt() = runBlocking { - val record = MutableLiveData() - testee.add(TAB_ID, record) - assertSame(record, testee.retrieveSiteData(TAB_ID)) - } - @Test fun whenDataDoesNotExistForTabThenRetrieveCreatesIt() { assertNotNull(testee.retrieveSiteData(TAB_ID)) @@ -185,21 +161,24 @@ class TabDataRepositoryTest { @Test fun whenTabDeletedThenTabAndDataCleared() = runBlocking { - val siteData = MutableLiveData() - testee.add(TAB_ID, siteData) + val addedTabId = testee.add() + val siteData = testee.retrieveSiteData(addedTabId) + + testee.delete(TabEntity(addedTabId, position = 0)) - testee.delete(TabEntity(TAB_ID, position = 0)) verify(mockDao).deleteTabAndUpdateSelection(any()) - assertNotSame(siteData, testee.retrieveSiteData(TAB_ID)) + assertNotSame(siteData, testee.retrieveSiteData(addedTabId)) } @Test fun whenAllDeletedThenTabAndDataCleared() = runBlocking { - val siteData = MutableLiveData() - testee.add(TAB_ID, siteData) + val addedTabId = testee.add() + val siteData = testee.retrieveSiteData(addedTabId) + testee.deleteAll() + verify(mockDao).deleteAllTabs() - assertNotSame(siteData, testee.retrieveSiteData(TAB_ID)) + assertNotSame(siteData, testee.retrieveSiteData(addedTabId)) } @Test @@ -216,7 +195,6 @@ class TabDataRepositoryTest { val captor = argumentCaptor() verify(mockDao).addAndSelectTab(captor.capture()) - assertTrue(captor.firstValue.position == 0) } @@ -231,23 +209,43 @@ class TabDataRepositoryTest { val captor = argumentCaptor() verify(mockDao).addAndSelectTab(captor.capture()) - assertTrue(captor.firstValue.position == 1) } + @Test + fun whenAddDefaultTabToExistingListOfTabsThenTabIsNotCreated() = runBlocking { + val db = createDatabase() + val dao = db.tabsDao() + testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister, useOurAppDetector) + testee.add("example.com") + + testee.addDefaultTab() + + assertTrue(testee.liveTabs.blockingObserve()?.size == 1) + } + + @Test + fun whenAddDefaultTabToEmptyTabsThenTabIsCreated() = runBlocking { + val db = createDatabase() + val dao = db.tabsDao() + testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister, useOurAppDetector) + + testee.addDefaultTab() + + assertTrue(testee.liveTabs.blockingObserve()?.size == 1) + } + @Test fun whenSelectByUrlOrNewTabIfUrlAlreadyExistedInATabThenSelectTheTab() = runBlocking { val db = createDatabase() val dao = db.tabsDao() dao.insertTab(TabEntity(tabId = "id", url = "http://www.example.com", skipHome = false, viewed = true, position = 0)) - testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister, useOurAppDetector) testee.selectByUrlOrNewTab("http://www.example.com") val value = testee.liveSelectedTab.blockingObserve()?.tabId assertEquals("id", value) - db.close() } @@ -255,14 +253,12 @@ class TabDataRepositoryTest { fun whenSelectByUrlOrNewTabIfUrlNotExistedInATabThenAddNewTab() = runBlocking { val db = createDatabase() val dao = db.tabsDao() - testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister, useOurAppDetector) testee.selectByUrlOrNewTab("http://www.example.com") val value = testee.liveSelectedTab.blockingObserve()?.url assertEquals("http://www.example.com", value) - db.close() } @@ -271,14 +267,12 @@ class TabDataRepositoryTest { val db = createDatabase() val dao = db.tabsDao() dao.insertTab(TabEntity(tabId = "id", url = "http://www.$USE_OUR_APP_DOMAIN/test", skipHome = false, viewed = true, position = 0)) - testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister, useOurAppDetector) testee.selectByUrlOrNewTab("http://m.$USE_OUR_APP_DOMAIN") val value = testee.liveSelectedTab.blockingObserve()?.tabId assertEquals("id", value) - db.close() } @@ -286,27 +280,25 @@ class TabDataRepositoryTest { fun whenSelectByUrlOrNewTabIfUrlNotExistedInATabAndUrlMatchesUseOurAppDomainThenAddNewTabWithCorrectUrl() = runBlocking { val db = createDatabase() val dao = db.tabsDao() - testee = TabDataRepository(dao, SiteFactory(mockPrivacyPractices, mockEntityLookup), mockWebViewPreviewPersister, useOurAppDetector) testee.selectByUrlOrNewTab("http://m.$USE_OUR_APP_DOMAIN") val value = testee.liveSelectedTab.blockingObserve()?.url assertEquals("http://m.$USE_OUR_APP_DOMAIN", value) - db.close() } @Test - fun whenAddWithSourceEnsureTabEntryContainsExpectedSourceId() = runBlocking { + fun whenAddFromSourceTabEnsureTabEntryContainsExpectedSourceId() = 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, useOurAppDetector) - val addedTabId = testee.addWithSource("http://www.example.com", skipHome = false, isDefaultTab = false) + val addedTabId = testee.addFromSourceTab("http://www.example.com", skipHome = false, sourceTabId = "sourceId") + val addedTab = testee.liveSelectedTab.blockingObserve() assertEquals(addedTabId, addedTab?.tabId) assertEquals(addedTab?.sourceTabId, sourceTab.tabId) @@ -320,12 +312,12 @@ class TabDataRepositoryTest { 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, useOurAppDetector) - var currentSelectedTabId = testee.liveSelectedTab.blockingObserve()?.tabId assertEquals(currentSelectedTabId, tabToDelete.tabId) + testee.deleteCurrentTabAndSelectSource() + currentSelectedTabId = testee.liveSelectedTab.blockingObserve()?.tabId assertEquals(currentSelectedTabId, sourceTab.tabId) } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index f7ae7d72d72d..e2199ad25ebd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -211,7 +211,7 @@ class BrowserActivity : DuckDuckGoActivity(), CoroutineScope by MainScope() { launch { viewModel.onOpenShortcut(sharedText) } } else { Timber.w("opening in new tab requested for $sharedText") - launch { viewModel.onOpenInNewTabRequested(sharedText, true) } + launch { viewModel.onOpenInNewTabRequested(query = sharedText, skipHome = true) } return } } @@ -289,13 +289,15 @@ class BrowserActivity : DuckDuckGoActivity(), CoroutineScope by MainScope() { launch { viewModel.onNewTabRequested() } } - fun openInNewTab(query: String) { - launch { viewModel.onOpenInNewTabRequested(query) } + fun openInNewTab(query: String, sourceTabId: String?) { + launch { + viewModel.onOpenInNewTabRequested(query = query, sourceTabId = sourceTabId) + } } - fun openMessageInNewTab(message: Message) { + fun openMessageInNewTab(message: Message, sourceTabId: String?) { openMessageInNewTabJob = launch { - val tabId = viewModel.onNewTabRequested() + val tabId = viewModel.onNewTabRequested(sourceTabId = sourceTabId) val fragment = openNewTab(tabId, null, false) fragment.messageFromPreviousTab = message } 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 c8b2cf7e92f0..4ec2966e4c9e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -470,10 +470,10 @@ class BrowserTabFragment : Fragment(), FindListener, CoroutineScope, DaxDialogLi when (it) { is Command.Refresh -> refresh() is Command.OpenInNewTab -> { - browserActivity?.openInNewTab(it.query) + browserActivity?.openInNewTab(it.query, it.sourceTabId) } is Command.OpenMessageInNewTab -> { - browserActivity?.openMessageInNewTab(it.message) + browserActivity?.openMessageInNewTab(it.message, it.sourceTabId) } is Command.OpenInNewBackgroundTab -> { openInNewBackgroundTab() 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 db73d0c8d4a0..46e79419ab04 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -51,32 +51,25 @@ import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType.IntentType import com.duckduckgo.app.browser.WebNavigationStateChange.* import com.duckduckgo.app.browser.addtohome.AddToHomeCapabilityDetector import com.duckduckgo.app.browser.favicon.FaviconDownloader -import com.duckduckgo.app.browser.logindetection.NavigationEvent import com.duckduckgo.app.browser.logindetection.LoginDetected import com.duckduckgo.app.browser.logindetection.NavigationAwareLoginDetector +import com.duckduckgo.app.browser.logindetection.NavigationEvent import com.duckduckgo.app.browser.model.BasicAuthenticationCredentials import com.duckduckgo.app.browser.model.BasicAuthenticationRequest import com.duckduckgo.app.browser.model.LongPressTarget import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter import com.duckduckgo.app.browser.session.WebViewSessionStorage import com.duckduckgo.app.browser.ui.HttpAuthenticationDialogFragment.HttpAuthenticationListener -import com.duckduckgo.app.cta.ui.Cta -import com.duckduckgo.app.cta.ui.CtaViewModel -import com.duckduckgo.app.cta.ui.DaxDialogCta -import com.duckduckgo.app.cta.ui.DialogCta -import com.duckduckgo.app.cta.ui.HomePanelCta -import com.duckduckgo.app.cta.ui.HomeTopPanelCta -import com.duckduckgo.app.cta.ui.UseOurAppCta +import com.duckduckgo.app.cta.ui.* import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepository import com.duckduckgo.app.global.* +import com.duckduckgo.app.global.events.db.UserEventKey +import com.duckduckgo.app.global.events.db.UserEventsStore import com.duckduckgo.app.global.model.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.global.events.db.UserEventsStore -import com.duckduckgo.app.global.events.db.UserEventKey -import com.duckduckgo.app.global.toDesktopUri import com.duckduckgo.app.global.useourapp.UseOurAppDetector import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_TITLE import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL @@ -212,8 +205,8 @@ class BrowserTabViewModel( class Navigate(val url: String) : Command() class NavigateBack(val steps: Int) : Command() object NavigateForward : Command() - class OpenInNewTab(val query: String) : Command() - class OpenMessageInNewTab(val message: Message) : Command() + class OpenInNewTab(val query: String, val sourceTabId: String? = null) : Command() + class OpenMessageInNewTab(val message: Message, val sourceTabId: String? = null) : Command() class OpenInNewBackgroundTab(val query: String) : Command() object LaunchNewTab : Command() object ResetHistory : Command() @@ -542,6 +535,7 @@ class BrowserTabViewModel( fun onUserPressedBack(): Boolean { navigationAwareLoginDetector.onEvent(NavigationEvent.UserAction.NavigateBack) val navigation = webNavigationState ?: return false + val hasSourceTab = tabRepository.liveSelectedTab.value?.sourceTabId != null if (currentFindInPageViewState().visible) { dismissFindInView() @@ -555,6 +549,11 @@ class BrowserTabViewModel( if (navigation.canGoBack) { command.value = NavigateBack(navigation.stepsToPreviousPage) return true + } else if (hasSourceTab) { + viewModelScope.launch { + tabRepository.deleteCurrentTabAndSelectSource() + } + return true } else if (!skipHome) { navigateHome() command.value = ShowKeyboard @@ -1034,7 +1033,7 @@ class BrowserTabViewModel( return when (requiredAction) { is RequiredAction.OpenInNewTab -> { command.value = GenerateWebViewPreviewImage - command.value = OpenInNewTab(requiredAction.url) + command.value = OpenInNewTab(query = requiredAction.url, sourceTabId = tabId) true } is RequiredAction.OpenInNewBackgroundTab -> { @@ -1312,12 +1311,8 @@ class BrowserTabViewModel( command.value = HandleExternalAppLink(appLink) } - override fun openInNewTab(url: String?) { - command.value = OpenInNewTab(url.orEmpty()) - } - override fun openMessageInNewTab(message: Message) { - command.value = OpenMessageInNewTab(message) + command.value = OpenMessageInNewTab(message, tabId) } override fun recoverFromRenderProcessGone() { 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 b37d4a3d4f72..84bae806e0fc 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -126,22 +126,33 @@ class BrowserViewModel( appEnjoymentPromptEmitter.promptType.observeForever(appEnjoymentObserver) } - suspend fun onNewTabRequested(isDefaultTab: Boolean = false): String { - return tabRepository.addWithSource(isDefaultTab = isDefaultTab) + suspend fun onNewTabRequested(sourceTabId: String? = null): String { + return if (sourceTabId != null) { + tabRepository.addFromSourceTab(sourceTabId = sourceTabId) + } else { + tabRepository.add() + } } - suspend fun onOpenInNewTabRequested(query: String, skipHome: Boolean = false): String { - return tabRepository.addWithSource( - queryUrlConverter.convertQueryToUrl(query), - skipHome, - isDefaultTab = false - ) + suspend fun onOpenInNewTabRequested(query: String, sourceTabId: String? = null, skipHome: Boolean = false): String { + return if (sourceTabId != null) { + tabRepository.addFromSourceTab( + url = queryUrlConverter.convertQueryToUrl(query), + skipHome = skipHome, + sourceTabId = sourceTabId + ) + } else { + tabRepository.add( + url = queryUrlConverter.convertQueryToUrl(query), + skipHome = skipHome + ) + } } suspend fun onTabsUpdated(tabs: List?) { - if (tabs == null || tabs.isEmpty()) { + if (tabs.isNullOrEmpty()) { Timber.i("Tabs list is null or empty; adding default tab") - tabRepository.add(isDefaultTab = true) + tabRepository.addDefaultTab() return } } 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 15859a2161ab..dba8ae8ea816 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/WebViewClientListener.kt @@ -42,7 +42,6 @@ interface WebViewClientListener { fun exitFullScreen() fun showFileChooser(filePathCallback: ValueCallback>, fileChooserParams: WebChromeClient.FileChooserParams) fun externalAppLinkClicked(appLink: SpecialUrlDetector.UrlType.IntentType) - fun openInNewTab(url: String?) fun openMessageInNewTab(message: Message) fun recoverFromRenderProcessGone() fun requiresAuthentication(request: BasicAuthenticationRequest) 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 ea4e529ffab0..736081db1ae4 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 @@ -33,12 +33,12 @@ import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.DismissedCta import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity +import com.duckduckgo.app.global.events.db.UserEventEntity +import com.duckduckgo.app.global.events.db.UserEventTypeConverter +import com.duckduckgo.app.global.events.db.UserEventsDao import com.duckduckgo.app.global.exception.UncaughtExceptionDao import com.duckduckgo.app.global.exception.UncaughtExceptionEntity import com.duckduckgo.app.global.exception.UncaughtExceptionSourceConverter -import com.duckduckgo.app.global.events.db.UserEventsDao -import com.duckduckgo.app.global.events.db.UserEventEntity -import com.duckduckgo.app.global.events.db.UserEventTypeConverter import com.duckduckgo.app.httpsupgrade.db.HttpsBloomFilterSpecDao import com.duckduckgo.app.httpsupgrade.db.HttpsWhitelistDao import com.duckduckgo.app.httpsupgrade.model.HttpsBloomFilterSpec @@ -318,7 +318,22 @@ class MigrationsProvider( val MIGRATION_23_TO_24: Migration = object : Migration(23, 24) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE `tabs` ADD COLUMN `sourceTabId` TEXT") + // https://stackoverflow.com/a/57797179/980345 + // SQLite does not support Alter table operations like Foreign keys + database.execSQL( + "CREATE TABLE IF NOT EXISTS tabs_new " + + "(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)," + + " FOREIGN KEY(sourceTabId) REFERENCES tabs(tabId) ON UPDATE SET NULL ON DELETE SET NULL )" + ) + database.execSQL( + "INSERT INTO tabs_new (tabId, url, title, skipHome, viewed, position, tabPreviewFile) " + + "SELECT tabId, url, title, skipHome, viewed, position, tabPreviewFile " + + "FROM tabs" + ) + database.execSQL("DROP TABLE tabs") + database.execSQL("ALTER TABLE tabs_new RENAME TO tabs") + database.execSQL("CREATE INDEX IF NOT EXISTS index_tabs_tabId ON tabs (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 92ba97f48674..7e1f085f1473 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 @@ -26,10 +26,8 @@ import com.duckduckgo.app.global.useourapp.UseOurAppDetector 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 @@ -49,29 +47,39 @@ class TabDataRepository @Inject constructor( private val siteData: LinkedHashMap> = LinkedHashMap() - override suspend fun add(url: String?, skipHome: Boolean, isDefaultTab: Boolean): String { + override suspend fun add(url: String?, skipHome: Boolean): String { val tabId = generateTabId() - add(tabId, buildSiteData(url), skipHome = skipHome, isDefaultTab = isDefaultTab) + add(tabId, buildSiteData(url), skipHome = skipHome, isDefaultTab = false) return tabId } - override suspend fun addWithSource(url: String?, skipHome: Boolean, isDefaultTab: Boolean): String { + override suspend fun addFromSourceTab(url: String?, skipHome: Boolean, sourceTabId: String): String { val tabId = generateTabId() - val sourceTabId = withContext(Dispatchers.IO) { - tabsDao.selectedTab()?.tabId - } add( - tabId, - buildSiteData(url), + tabId = tabId, + data = buildSiteData(url), skipHome = skipHome, - isDefaultTab = isDefaultTab, + isDefaultTab = false, sourceTabId = sourceTabId ) return tabId } + override suspend fun addDefaultTab(): String { + val tabId = generateTabId() + + add( + tabId = tabId, + data = buildSiteData(null), + skipHome = false, + isDefaultTab = true + ) + + return tabId + } + private fun generateTabId() = UUID.randomUUID().toString() private fun buildSiteData(url: String?): MutableLiveData { @@ -83,7 +91,7 @@ class TabDataRepository @Inject constructor( return data } - override suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean, isDefaultTab: Boolean, sourceTabId: String?) { + private fun add(tabId: String, data: MutableLiveData, skipHome: Boolean, isDefaultTab: Boolean, sourceTabId: String? = null) { siteData[tabId] = data databaseExecutor().scheduleDirect { @@ -102,7 +110,8 @@ class TabDataRepository @Inject constructor( } Timber.i("About to add a new tab, isDefaultTab: $isDefaultTab. $tabId, position: $position") - tabsDao.addAndSelectTab(TabEntity( + tabsDao.addAndSelectTab( + TabEntity( tabId = tabId, url = data.value?.url, title = data.value?.title, @@ -110,7 +119,8 @@ class TabDataRepository @Inject constructor( viewed = true, position = position, sourceTabId = sourceTabId - )) + ) + ) } } @@ -125,7 +135,7 @@ class TabDataRepository @Inject constructor( if (tabId != null) { select(tabId) } else { - add(url, skipHome = true, isDefaultTab = false) + add(url, skipHome = true) } } @@ -140,7 +150,8 @@ class TabDataRepository @Inject constructor( title = title, skipHome = false, viewed = false, - position = position + 1 + position = position + 1, + sourceTabId = tabId ) tabsDao.insertTabAtPosition(tab) } @@ -178,10 +189,10 @@ class TabDataRepository @Inject constructor( deleteOldPreviewImages(tabToDelete.tabId) val tabToSelect = tabToDelete.sourceTabId - .takeUnless { it.isNullOrBlank() } - ?.let { - tabsDao.tab(it) - } + .takeUnless { it.isNullOrBlank() } + ?.let { + tabsDao.tab(it) + } tabsDao.deleteTabAndUpdateSelection(tabToDelete, tabToSelect) siteData.remove(tabToDelete.tabId) } 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 4a4cd53958be..5acd7918bcfd 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 @@ -17,11 +17,20 @@ package com.duckduckgo.app.tabs.model import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey @Entity( tableName = "tabs", + foreignKeys = [ + ForeignKey( + entity = TabEntity::class, + parentColumns = ["tabId"], + childColumns = ["sourceTabId"], + onDelete = ForeignKey.SET_NULL, + onUpdate = ForeignKey.SET_NULL + )], indices = [ Index("tabId") ] 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 84b87a3941c3..26a60acd9ad7 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 @@ -29,11 +29,11 @@ interface TabRepository { /** * @return tabId of new record */ - suspend fun add(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false): String + suspend fun add(url: String? = null, skipHome: Boolean = false): String - suspend fun add(tabId: String, data: MutableLiveData, skipHome: Boolean = false, isDefaultTab: Boolean = false, sourceTabId: String? = null) + suspend fun addDefaultTab(): String - suspend fun addWithSource(url: String? = null, skipHome: Boolean = false, isDefaultTab: Boolean = false): String + suspend fun addFromSourceTab(url: String? = null, skipHome: Boolean = false, sourceTabId: String): String suspend fun addNewTabAfterExistingTab(url: String? = null, tabId: String)