Skip to content

Conversation

@JKane10
Copy link
Contributor

@JKane10 JKane10 commented Jul 4, 2020

Task/Issue URL: #753

Description:

Approach:

  1. Modified TabEntity to maintain a sourceTabId to reference the source tab by its tab id.
  2. When opening a new tab, store the sourceTabId in the TabEntity db entry.
  3. In the event that a download is opened in a new, blank tab (url is blank), callbacks will use tabRepo to select the source tab and delete the blank tab.

Concerns

  • This still opens a blank tab in which the download options are displayed, but upon the user completing their selections, the blank tab is cleaned up and the source tab is selected and displayed.
  • TabEntities now maintain a source whether it is needed or not - Could simplify this a bit by just passing the sourceTabId in the Fragment bundle as well?
  • LeakCanary is identifying a leak, but struggling to track it down. If this approach is valid, could use some assistance on this.

Steps to test this PR:

  1. Navigate to : https://www.w3schools.com/code/tryit.asp?filename=GGETHXPDSEMN
  2. Run and select "Download in new tab".
  3. Make the appropriate selection
  4. New, blank tab should close and return to source tab.

753

JKane10 added 5 commits July 4, 2020 01:15
…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.
…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
@cmonfortep cmonfortep self-assigned this Jul 6, 2020
@cmonfortep
Copy link
Contributor

Thanks for the contribution, I will review it this week.

@cmonfortep
Copy link
Contributor

@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.

Copy link
Contributor

@cmonfortep cmonfortep left a 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.

…anup and referencing the liveSelectedTab from inside the repo rather than duplicating the value and passing as a param
@JKane10 JKane10 requested a review from cmonfortep July 29, 2020 14:27
Copy link
Contributor

@cmonfortep cmonfortep left a 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
Copy link
Contributor

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().

Copy link
Contributor Author

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
Copy link
Contributor

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.
Copy link
Contributor

@cmonfortep cmonfortep left a 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 ->
Copy link
Contributor

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.

Copy link
Contributor Author

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))
Copy link
Contributor

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) {
Copy link
Contributor

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
        }

Copy link
Contributor Author

@JKane10 JKane10 Aug 1, 2020

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!

@JKane10
Copy link
Contributor Author

JKane10 commented Aug 1, 2020

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.

@JKane10 JKane10 requested a review from cmonfortep August 1, 2020 03:50
Copy link
Contributor

@cmonfortep cmonfortep left a 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.

JKane10 added 2 commits August 3, 2020 13:05
…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
@JKane10 JKane10 requested a review from cmonfortep August 3, 2020 18:16
@JKane10
Copy link
Contributor Author

JKane10 commented Aug 3, 2020

@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.

The DB implementation is really well done. Updating the migration and test seems straight forward. Let me know if I missed anything.

Copy link
Contributor

@cmonfortep cmonfortep left a 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. 🙌

@cmonfortep cmonfortep merged commit 408259c into duckduckgo:develop Aug 5, 2020
@JKane10
Copy link
Contributor Author

JKane10 commented Aug 5, 2020

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants