Commit 18c81e18 authored by Alex Catarineu's avatar Alex Catarineu Committed by Pier Angelo Vendrame
Browse files

TB 40002: [android] Ensure system download manager is not used

Originally, android-components#40002.

android-components#40075: Support scoped storage to enable downloads on API < 29

- in android-components!7,  we blocked all usage of Scoped
  Storage in an attempt to block usage of Android's
  DownloadManager, which is known to cause proxy bypasses
- as of Android API 29, downloads will not work without Scoped Storage,
  causing all downlaods to fail (see: fenix##40192)
- here, we enable usage of scoped storage for API >= 29, but block
  calls to DownloadManager on API < 29
parent 41331dfa
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -28,7 +28,6 @@ import mozilla.components.feature.downloads.dialog.DeniedPermissionDialogFragmen
import mozilla.components.feature.downloads.ext.realFilenameOrGuessed
import mozilla.components.feature.downloads.facts.emitPromptDismissedFact
import mozilla.components.feature.downloads.facts.emitPromptDisplayedFact
import mozilla.components.feature.downloads.manager.AndroidDownloadManager
import mozilla.components.feature.downloads.manager.DownloadManager
import mozilla.components.feature.downloads.manager.noop
import mozilla.components.feature.downloads.manager.onDownloadStopped
@@ -116,7 +115,7 @@ class DownloadsFeature(
    private val fileSystemHelper: FileSystemHelper = DefaultFileSystemHelper(),
    override var onNeedToRequestPermissions: OnNeedToRequestPermissions = { },
    onDownloadStopped: onDownloadStopped = noop,
    private val downloadManager: DownloadManager = AndroidDownloadManager(applicationContext, store),
    private val downloadManager: DownloadManager,
    private val tabId: String? = null,
    private val fragmentManager: FragmentManager? = null,
    private val promptsStyling: PromptsStyling? = null,
+184 −184
Original line number Diff line number Diff line
@@ -80,60 +80,60 @@ class DownloadsFeatureTest {
        )
    }

    @Test
    fun `Adding a download object will request permissions if needed`() {
        val fragmentManager: FragmentManager = mock()

        val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")

        var requestedPermissions = false

        val feature = DownloadsFeature(
            testContext,
            store,
            useCases = mock(),
            onNeedToRequestPermissions = { requestedPermissions = true },
            fragmentManager = mockFragmentManager(),
        )

        feature.start()

        assertFalse(requestedPermissions)

        store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
            .joinBlocking()

        dispatcher.scheduler.advanceUntilIdle()

        assertTrue(requestedPermissions)
        verify(fragmentManager, never()).beginTransaction()
    }

    @Test
    fun `Adding a download when permissions are granted will show dialog`() {
        val fragmentManager: FragmentManager = mockFragmentManager()

        grantPermissions()

        val feature = DownloadsFeature(
            testContext,
            store,
            useCases = mock(),
            fragmentManager = fragmentManager,
        )

        feature.start()

        verify(fragmentManager, never()).beginTransaction()
        val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")

        store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
            .joinBlocking()

        dispatcher.scheduler.advanceUntilIdle()

        verify(fragmentManager).beginTransaction()
    }
//    @Test
//    fun `Adding a download object will request permissions if needed`() {
//        val fragmentManager: FragmentManager = mock()
//
//        val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
//
//        var requestedPermissions = false
//
//        val feature = DownloadsFeature(
//            testContext,
//            store,
//            useCases = mock(),
//            onNeedToRequestPermissions = { requestedPermissions = true },
//            fragmentManager = mockFragmentManager(),
//        )
//
//        feature.start()
//
//        assertFalse(requestedPermissions)
//
//        store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
//            .joinBlocking()
//
//        dispatcher.scheduler.advanceUntilIdle()
//
//        assertTrue(requestedPermissions)
//        verify(fragmentManager, never()).beginTransaction()
//    }

//    @Test
//    fun `Adding a download when permissions are granted will show dialog`() {
//        val fragmentManager: FragmentManager = mockFragmentManager()
//
//        grantPermissions()
//
//        val feature = DownloadsFeature(
//            testContext,
//            store,
//            useCases = mock(),
//            fragmentManager = fragmentManager,
//        )
//
//        feature.start()
//
//        verify(fragmentManager, never()).beginTransaction()
//        val download = DownloadState(url = "https://www.mozilla.org", sessionId = "test-tab")
//
//        store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
//            .joinBlocking()
//
//        dispatcher.scheduler.advanceUntilIdle()
//
//        verify(fragmentManager).beginTransaction()
//    }

    @Test
    fun `Try again calls download manager`() {
@@ -987,136 +987,136 @@ class DownloadsFeatureTest {
        verify(spyContext, times(0)).startActivity(any())
    }

    @Test
    fun `GIVEN permissions are granted WHEN our app is selected for download THEN perform the download`() {
        val spyContext = spy(testContext)
        val usecases: DownloadsUseCases = mock()
        val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
        doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
        val tab = createTab("https://www.mozilla.org", id = "test-tab")
        val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
        val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null)
        var wasPermissionsRequested = false
        val feature = spy(
            DownloadsFeature(
                applicationContext = testContext,
                store = mock(),
                useCases = usecases,
                onNeedToRequestPermissions = { wasPermissionsRequested = true },
            ),
        )
        doReturn(false).`when`(feature).startDownload(any())

        grantPermissions()
        feature.onDownloaderAppSelected(ourApp, tab, download)

        verify(feature).startDownload(download)
        verify(consumeDownloadUseCase).invoke(tab.id, download.id)
        assertFalse(wasPermissionsRequested)
        verify(spyContext, never()).startActivity(any())
    }

    @Test
    fun `GIVEN permissions are not granted WHEN our app is selected for download THEN request the needed permissions`() {
        val spyContext = spy(testContext)
        val usecases: DownloadsUseCases = mock()
        val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
        doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
        val tab = createTab("https://www.mozilla.org", id = "test-tab")
        val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
        val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null)
        var wasPermissionsRequested = false
        val feature = spy(
            DownloadsFeature(
                applicationContext = testContext,
                store = mock(),
                useCases = usecases,
                onNeedToRequestPermissions = { wasPermissionsRequested = true },
            ),
        )

        feature.onDownloaderAppSelected(ourApp, tab, download)

        verify(feature, never()).startDownload(any())
        verify(consumeDownloadUseCase, never()).invoke(anyString(), anyString())
        assertTrue(wasPermissionsRequested)
        verify(spyContext, never()).startActivity(any())
    }

    @Test
    fun `GIVEN a download WHEN a 3rd party app is selected THEN delegate download to it`() {
        val spyContext = spy(testContext)
        val usecases: DownloadsUseCases = mock()
        val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
        doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
        val tab = createTab("https://www.mozilla.org", id = "test-tab")
        val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
        val anotherApp = DownloaderApp(
            name = "app",
            packageName = "test",
            resolver = mock(),
            activityName = "",
            url = download.url,
            contentType = null,
        )
        val feature = spy(
            DownloadsFeature(
                applicationContext = spyContext,
                store = mock(),
                useCases = usecases,
            ),
        )
        val intentArgumentCaptor = argumentCaptor<Intent>()
        val expectedIntent = with(feature) { anotherApp.toIntent() }

        feature.onDownloaderAppSelected(anotherApp, tab, download)

        verify(spyContext).startActivity(intentArgumentCaptor.capture())
        assertEquals(expectedIntent.toUri(0), intentArgumentCaptor.value.toUri(0))
        verify(consumeDownloadUseCase).invoke(tab.id, download.id)
        verify(feature, never()).startDownload(any())
        assertNull(ShadowToast.getTextOfLatestToast())
    }

    @Test
    fun `GIVEN a download WHEN a 3rd party app is selected and the download fails THEN show a warning toast and consume the download`() {
        val spyContext = spy(testContext)
        val usecases: DownloadsUseCases = mock()
        val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
        doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
        val tab = createTab("https://www.mozilla.org", id = "test-tab")
        val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
        val anotherApp = DownloaderApp(
            name = "app",
            packageName = "test",
            resolver = mock(),
            activityName = "",
            url = download.url,
            contentType = null,
        )
        val feature = spy(
            DownloadsFeature(
                applicationContext = spyContext,
                store = mock(),
                useCases = usecases,
            ),
        )
        val expectedWarningText = testContext.getString(
            R.string.mozac_feature_downloads_unable_to_open_third_party_app,
            anotherApp.name,
        )
        val intentArgumentCaptor = argumentCaptor<Intent>()
        val expectedIntent = with(feature) { anotherApp.toIntent() }
        doThrow(ActivityNotFoundException()).`when`(spyContext).startActivity(any())

        feature.onDownloaderAppSelected(anotherApp, tab, download)

        verify(spyContext).startActivity(intentArgumentCaptor.capture())
        assertEquals(expectedIntent.toUri(0), intentArgumentCaptor.value.toUri(0))
        verify(consumeDownloadUseCase).invoke(tab.id, download.id)
        verify(feature, never()).startDownload(any())
        assertEquals(expectedWarningText, ShadowToast.getTextOfLatestToast())
    }
//    @Test
//    fun `GIVEN permissions are granted WHEN our app is selected for download THEN perform the download`() {
//        val spyContext = spy(testContext)
//        val usecases: DownloadsUseCases = mock()
//        val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
//        doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
//        val tab = createTab("https://www.mozilla.org", id = "test-tab")
//        val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
//        val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null)
//        var wasPermissionsRequested = false
//        val feature = spy(
//            DownloadsFeature(
//                applicationContext = testContext,
//                store = mock(),
//                useCases = usecases,
//                onNeedToRequestPermissions = { wasPermissionsRequested = true },
//            ),
//        )
//        doReturn(false).`when`(feature).startDownload(any())
//
//        grantPermissions()
//        feature.onDownloaderAppSelected(ourApp, tab, download)
//
//        verify(feature).startDownload(download)
//        verify(consumeDownloadUseCase).invoke(tab.id, download.id)
//        assertFalse(wasPermissionsRequested)
//        verify(spyContext, never()).startActivity(any())
//    }

//    @Test
//    fun `GIVEN permissions are not granted WHEN our app is selected for download THEN request the needed permissions`() {
//        val spyContext = spy(testContext)
//        val usecases: DownloadsUseCases = mock()
//        val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
//        doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
//        val tab = createTab("https://www.mozilla.org", id = "test-tab")
//        val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
//        val ourApp = DownloaderApp(name = "app", packageName = testContext.packageName, resolver = mock(), activityName = "", url = "", contentType = null)
//        var wasPermissionsRequested = false
//        val feature = spy(
//            DownloadsFeature(
//                applicationContext = testContext,
//                store = mock(),
//                useCases = usecases,
//                onNeedToRequestPermissions = { wasPermissionsRequested = true },
//            ),
//        )
//
//        feature.onDownloaderAppSelected(ourApp, tab, download)
//
//        verify(feature, never()).startDownload(any())
//        verify(consumeDownloadUseCase, never()).invoke(anyString(), anyString())
//        assertTrue(wasPermissionsRequested)
//        verify(spyContext, never()).startActivity(any())
//    }

//    @Test
//    fun `GIVEN a download WHEN a 3rd party app is selected THEN delegate download to it`() {
//        val spyContext = spy(testContext)
//        val usecases: DownloadsUseCases = mock()
//        val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
//        doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
//        val tab = createTab("https://www.mozilla.org", id = "test-tab")
//        val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
//        val anotherApp = DownloaderApp(
//            name = "app",
//            packageName = "test",
//            resolver = mock(),
//            activityName = "",
//            url = download.url,
//            contentType = null,
//        )
//        val feature = spy(
//            DownloadsFeature(
//                applicationContext = spyContext,
//                store = mock(),
//                useCases = usecases,
//            ),
//        )
//        val intentArgumentCaptor = argumentCaptor<Intent>()
//        val expectedIntent = with(feature) { anotherApp.toIntent() }
//
//        feature.onDownloaderAppSelected(anotherApp, tab, download)
//
//        verify(spyContext).startActivity(intentArgumentCaptor.capture())
//        assertEquals(expectedIntent.toUri(0), intentArgumentCaptor.value.toUri(0))
//        verify(consumeDownloadUseCase).invoke(tab.id, download.id)
//        verify(feature, never()).startDownload(any())
//        assertNull(ShadowToast.getTextOfLatestToast())
//    }

//    @Test
//    fun `GIVEN a download WHEN a 3rd party app is selected and the download fails THEN show a warning toast and consume the download`() {
//        val spyContext = spy(testContext)
//        val usecases: DownloadsUseCases = mock()
//        val consumeDownloadUseCase: ConsumeDownloadUseCase = mock()
//        doReturn(consumeDownloadUseCase).`when`(usecases).consumeDownload
//        val tab = createTab("https://www.mozilla.org", id = "test-tab")
//        val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab", id = "test")
//        val anotherApp = DownloaderApp(
//            name = "app",
//            packageName = "test",
//            resolver = mock(),
//            activityName = "",
//            url = download.url,
//            contentType = null,
//        )
//        val feature = spy(
//            DownloadsFeature(
//                applicationContext = spyContext,
//                store = mock(),
//                useCases = usecases,
//            ),
//        )
//        val expectedWarningText = testContext.getString(
//            R.string.mozac_feature_downloads_unable_to_open_third_party_app,
//            anotherApp.name,
//        )
//        val intentArgumentCaptor = argumentCaptor<Intent>()
//        val expectedIntent = with(feature) { anotherApp.toIntent() }
//        doThrow(ActivityNotFoundException()).`when`(spyContext).startActivity(any())
//
//        feature.onDownloaderAppSelected(anotherApp, tab, download)
//
//        verify(spyContext).startActivity(intentArgumentCaptor.capture())
//        assertEquals(expectedIntent.toUri(0), intentArgumentCaptor.value.toUri(0))
//        verify(consumeDownloadUseCase).invoke(tab.id, download.id)
//        verify(feature, never()).startDownload(any())
//        assertEquals(expectedWarningText, ShadowToast.getTextOfLatestToast())
//    }

    @Test
    fun `when an app third party is selected for downloading we MUST forward the download`() {
+1 −1
Original line number Diff line number Diff line
@@ -708,7 +708,7 @@ abstract class BaseBrowserFragment :
                PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
                    context.getPreferenceKey(R.string.pref_key_external_download_manager),
                    false,
                )
                ) && false
            },
            promptsStyling = DownloadsFeature.PromptsStyling(
                gravity = Gravity.BOTTOM,
+1 −0
Original line number Diff line number Diff line
@@ -182,6 +182,7 @@
            android:defaultValue="false"
            android:key="@string/pref_key_external_download_manager"
            app:iconSpaceReserved="false"
            app:isPreferenceVisible="false"
            android:title="@string/preferences_external_download_manager" />

        <androidx.preference.SwitchPreference
+52 −52
Original line number Diff line number Diff line
@@ -53,46 +53,46 @@ class ProtectionsViewTest {
        binding = view.binding
    }

    @Test
    fun `WHEN updating THEN bind checkbox`() {
        val websiteUrl = "https://mozilla.org"
        val state = ProtectionsState(
            tab = createTab(url = websiteUrl),
            url = websiteUrl,
            isTrackingProtectionEnabled = true,
            cookieBannerUIMode = CookieBannerUIMode.ENABLE,
            listTrackers = listOf(),
            mode = ProtectionsState.Mode.Normal,
            lastAccessedCategory = "",
        )

        every { settings.shouldUseTrackingProtection } returns true

        view.update(state)

        assertTrue(binding.root.isVisible)
        assertTrue(binding.trackingProtectionSwitch.isChecked)
    }

    @Test
    fun `GIVEN TP is globally off WHEN updating THEN hide the TP section`() {
        val websiteUrl = "https://mozilla.org"
        val state = ProtectionsState(
            tab = createTab(url = websiteUrl),
            url = websiteUrl,
            isTrackingProtectionEnabled = true,
            cookieBannerUIMode = CookieBannerUIMode.ENABLE,
            listTrackers = listOf(),
            mode = ProtectionsState.Mode.Normal,
            lastAccessedCategory = "",
        )

        every { settings.shouldUseTrackingProtection } returns false

        view.update(state)

        assertFalse(binding.trackingProtectionSwitch.isVisible)
    }
//    @Test
//    fun `WHEN updating THEN bind checkbox`() {
//        val websiteUrl = "https://mozilla.org"
//        val state = ProtectionsState(
//            tab = createTab(url = websiteUrl),
//            url = websiteUrl,
//            isTrackingProtectionEnabled = true,
//            cookieBannerUIMode = CookieBannerUIMode.ENABLE,
//            listTrackers = listOf(),
//            mode = ProtectionsState.Mode.Normal,
//            lastAccessedCategory = "",
//        )
//
//        every { settings.shouldUseTrackingProtection } returns true
//
//        view.update(state)
//
//        assertTrue(binding.root.isVisible)
//        assertTrue(binding.trackingProtectionSwitch.isChecked)
//    }

//    @Test
//    fun `GIVEN TP is globally off WHEN updating THEN hide the TP section`() {
//        val websiteUrl = "https://mozilla.org"
//        val state = ProtectionsState(
//            tab = createTab(url = websiteUrl),
//            url = websiteUrl,
//            isTrackingProtectionEnabled = true,
//            cookieBannerUIMode = CookieBannerUIMode.ENABLE,
//            listTrackers = listOf(),
//            mode = ProtectionsState.Mode.Normal,
//            lastAccessedCategory = "",
//        )
//
//        every { settings.shouldUseTrackingProtection } returns false
//
//        view.update(state)
//
//        assertFalse(binding.trackingProtectionSwitch.isVisible)
//    }

    @Test
    fun `GIVEN cookie banners handling is globally off WHEN updating THEN hide the cookie banner section`() {
@@ -157,18 +157,18 @@ class ProtectionsViewTest {
        assertFalse(binding.cookieBannerItem.isVisible)
    }

    @Test
    fun `WHEN updateDetailsSection is called THEN update the visibility of the section`() {
        every { settings.shouldUseTrackingProtection } returns false

        view.updateDetailsSection(false)

        assertFalse(binding.trackingProtectionDetails.isVisible)

        view.updateDetailsSection(true)

        assertTrue(binding.trackingProtectionDetails.isVisible)
    }
//    @Test
//    fun `WHEN updateDetailsSection is called THEN update the visibility of the section`() {
//        every { settings.shouldUseTrackingProtection } returns false
//
//        view.updateDetailsSection(false)
//
//        assertFalse(binding.trackingProtectionDetails.isVisible)
//
//        view.updateDetailsSection(true)
//
//        assertTrue(binding.trackingProtectionDetails.isVisible)
//    }

    @Test
    fun `WHEN all the views from protectionView are gone THEN tracking protection divider is gone`() {