-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Issue #753 - Downloads that open in a new tab leave user on an empty tab #883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Issue #753 - Downloads that open in a new tab leave user on an empty tab #883
Conversation
…o 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.
…leteAndSelectSource
…753-download-in-new-tab-leaves-behind-blank-tab � Conflicts: � app/schemas/com.duckduckgo.app.global.db.AppDatabase/22.json � app/src/androidTest/java/com/duckduckgo/app/global/db/AppDatabaseTest.kt � app/src/main/java/com/duckduckgo/app/global/db/AppDatabase.kt
|
Thanks for the contribution, I will review it this week. |
|
@JKane10 Sorry for the delay here. I told you I was going to review your PR a few weeks ago and I didn't have time for it + personal time off. I'm back to the party and I will review it ASAP. |
cmonfortep
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @JKane10! Thanks for this contribution.
I took a look to your approach and I think it's pretty good! Storing the sourceTabId in the database makes possible to close the blank page and go back to the previous tab. Not only for this case but also for future improvements we will introduce to deal with the "back button".
Here you have some comments from my side. I wanted to share with you some thoughts about how to get the current Tab Id, let me know what do you think.
About the leak, I think the one you are saying is not related to your changes (I saw one related to the constraintLayout), so no need for you to focus there.
Looking forward to work with you here.
app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt
Outdated
Show resolved
Hide resolved
…anup and referencing the liveSelectedTab from inside the repo rather than duplicating the value and passing as a param
cmonfortep
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was taking a quick look to the changes and I had only one comment, but the rest looks fine. I will wait for that change and do a final full review + testing. Thanks!
| buildSiteData(url), | ||
| skipHome = skipHome, | ||
| isDefaultTab = isDefaultTab, | ||
| sourceTabId = liveSelectedTab.value?.tabId |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting the value from liveSelectedTab works because when we call addWithSource (or deleteCurrentTabAndSelectSource) a ViewModel is observing that LiveData externally. But if we end up in a scenario where we call this method without anyone observing the LiveData, this will fail. I think is better to use tabsDao.selectedTab().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, let me know how my latest approach looks. A bit new to coroutines still, so not sure if the usage of withContext is ideal, but we'd need to await the value from the DB before proceeding and can't access the DB on the main thread.
| } | ||
|
|
||
| override suspend fun deleteCurrentTabAndSelectSource() { | ||
| val tabToDelete = liveSelectedTab.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here with this LiveData.
… directly from the tabsDao rather than the LiveData object.
cmonfortep
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great. I think we are close to being done 🙌
There's only one more comment from my side, so if you could address it and test that we haven't broken anything we are good.
Also, would you mind to add a few tests in TabDataRepositoryTest for the methods we introduced in this PR? just to cover the new logic.
Awesome! thanks!
| siteData.remove(tabToDelete.tabId) | ||
| } | ||
|
|
||
| tabToDelete?.sourceTabId?.let { sourceTabId -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking twice this method, I was asking myself what will happen if tabToDelete?.sourceTabId is null. It will not crash but we will not select a new tab. It may produce an inconsistent state for users.
Additionally, we are doing a few operations that modify the database (delete + insert), but they are not inside a transaction. If one fails, we could leave the database in a weird state too.
To solve both things I was thinking about modifying tabsDao.deleteTabAndUpdateSelection, where we can have a @Transaction, and also a fallback to the firstTab.
@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))
}
}
and the repository method will look like:
override suspend fun deleteCurrentTabAndSelectSource() {
databaseExecutor().scheduleDirect {
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)
}
}
I haven't checked if that actually works or if it compiles 😅, just wanted to show the idea. What do you think? please double check from your side too in case you like the approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, would definitely want to ensure the entire transaction completes rather than risk leaving the DB in an undesired state. The code you provided works as is and looks good to me.
This also accounts for the unlikely situation that a user opens a link in the new tab, declines the dialog, closes the sourceTab, and then refreshes the download tab to reinitialize the download dialog and proceeds. The user will now return to the firstTab where as previously the user would just remain on the blank new tab.
| 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, sourceTabId)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not important: I see you added null as previewImage because you wanted to pass sourceTabId which comes next. To avoid that, you could use named params instead, and only send the ones you know its value.
tabsDao.addAndSelectTab(
TabEntity(
tabId = tabId,
url = data.value?.url,
title = data.value?.title,
skipHome = skipHome,
viewed = true,
position = position,
sourceTabId = sourceTabId
)
)
| val tabId = generateTabId() | ||
| var sourceTabId: String? = null | ||
|
|
||
| withContext(Dispatchers.IO) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 you can perfectly use withContext here to change threads.
Because you mentioned that you were a bit new to coroutines, just a small nice tip 🙌.
If you check the signature of withContext:
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
That means withContext can return the value you are querying in the internal block.
You can do this:
val sourceTabId = withContext(Dispatchers.IO) {
tabsDao.selectedTab()?.tabId
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, this is cleaner too!
…te db calls to ensure data integrity.
…eCurrentTabAndSelectSource)
|
Made changes related to the latest feedback and added 2 tests for the 2 new TabDataRepo methods. Let me know if the tests look sufficient. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JKane10 This looks great! I've also tested the latest revision and works as expected. Good job!
Sadly, I've just noticed we merged today a PR that has updated the database version, so you have some conflicts to solve here. Could you solve them? I will merge the PR after that.
Solving database code conflicts is a bit tricky: you will need to accept the 23.json from develop, update your database versions to 24, and that will generate a new json file for the database changes we introduced here. Also, update the database test cases to cover migration from 23 to 24. If it gets hairy, let me know and I can solve them for you. Happy to help here.
…753-download-in-new-tab-leaves-behind-blank-tab � Conflicts: � app/schemas/com.duckduckgo.app.global.db.AppDatabase/23.json � app/src/androidTest/java/com/duckduckgo/app/tabs/model/TabDataRepositoryTest.kt
The DB implementation is really well done. Updating the migration and test seems straight forward. Let me know if I missed anything. |
cmonfortep
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JKane10 Thanks for the contribution!!! Really enjoyed working with you here. We have been really looking forward to fixing this bug, so thanks for the changes. 🙌
Thanks for working with me on this @cmonfortep. Enjoyed contributing. I'll keep my eye out for other opportunities. |
Task/Issue URL: #753
Description:
Approach:
Concerns
Steps to test this PR: