Commit 3f2a5d60 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 9b99a09c
Loading
Loading
Loading
Loading
+1 −2
Changes for mobile/android/android-components/components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadsFeature.kt: 1 added line, 2 removed lines.
Original line number Diff line number Diff line
@@ -30,7 +30,6 @@ import mozilla.components.feature.downloads.dialog.DeniedPermissionDialogFragmen
import mozilla.components.feature.downloads.ext.getRealFilenameOrGuessed
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
@@ -115,7 +114,7 @@ class DownloadsFeature(
    override var onNeedToRequestPermissions: OnNeedToRequestPermissions = {},
    onDownloadStopped: onDownloadStopped = noop,
    private val downloadFileUtils: DownloadFileUtils,
    private val downloadManager: DownloadManager = AndroidDownloadManager(applicationContext, store, downloadFileUtils),
    private val downloadManager: DownloadManager,
    private val tabId: String? = null,
    private val fragmentManager: FragmentManager? = null,
    private val promptsStyling: PromptsStyling? = null,
+159 −221
Changes for mobile/android/android-components/components/feature/downloads/src/test/java/mozilla/components/feature/downloads/DownloadsFeatureTest.kt: 159 added lines, 221 removed lines.
Original line number Diff line number Diff line
@@ -77,68 +77,6 @@ class DownloadsFeatureTest {
            )
    }

    @Test
    fun `Adding a download object will request permissions if needed`() =
        runTest(testDispatcher) {
            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(),
                    mainDispatcher = testDispatcher,
                    downloadFileUtils = FakeDownloadFileUtils(),
                )

            feature.start()
            testDispatcher.scheduler.advanceUntilIdle()

            assertFalse(requestedPermissions)

            store.dispatch(ContentAction.UpdateDownloadAction("test-tab", download))
            testDispatcher.scheduler.advanceUntilIdle()

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

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

            grantPermissions()

            val feature =
                DownloadsFeature(
                    testContext,
                    store,
                    useCases = mock(),
                    fragmentManager = fragmentManager,
                    mainDispatcher = testDispatcher,
                    downloadFileUtils = FakeDownloadFileUtils(),
                )

            feature.start()
            testDispatcher.scheduler.advanceUntilIdle()

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

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

            testDispatcher.scheduler.advanceUntilIdle()

            verify(fragmentManager).beginTransaction()
        }

    @Test
    fun `Try again calls download manager`() =
        runTest(testDispatcher) {
@@ -1301,85 +1239,85 @@ 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`() =
        runTest(testDispatcher) {
            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 = BrowserStore(),
                        useCases = usecases,
                        mainDispatcher = testDispatcher,
                        downloadFileUtils = FakeDownloadFileUtils(),
                        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`() =
        runTest(testDispatcher) {
            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 = BrowserStore(),
                        useCases = usecases,
                        mainDispatcher = testDispatcher,
                        downloadFileUtils = FakeDownloadFileUtils(),
                        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 permissions are granted WHEN our app is selected for download THEN perform the download`() =
//        runTest(testDispatcher) {
//            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 = BrowserStore(),
//                        useCases = usecases,
//                        mainDispatcher = testDispatcher,
//                        downloadFileUtils = FakeDownloadFileUtils(),
//                        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`() =
//        runTest(testDispatcher) {
//            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 = BrowserStore(),
//                        useCases = usecases,
//                        mainDispatcher = testDispatcher,
//                        downloadFileUtils = FakeDownloadFileUtils(),
//                        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`() =
@@ -1421,86 +1359,86 @@ class DownloadsFeatureTest {
            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`() =
        runTest(testDispatcher) {
            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 = BrowserStore(),
                        useCases = usecases,
                        mainDispatcher = testDispatcher,
                        downloadFileUtils = FakeDownloadFileUtils(),
                    )
                )
            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`() =
        runTest(testDispatcher) {
            val spyContext = spy(testContext)
            val tab = createTab("https://www.mozilla.org", id = "test-tab")
            val downloadsUseCases = spy(DownloadsUseCases(store, mock()))
            val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>()
            val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
            val ourApp =
                DownloaderApp(
                    name = "app",
                    packageName = "thridparty.app",
                    resolver = mock(),
                    activityName = "",
                    url = "",
                    contentType = null,
                )
            val anotherApp = mock<DownloaderApp>()
            val apps = listOf(ourApp, anotherApp)
            val dialog = DownloadAppChooserDialog()
            val fragmentManager: FragmentManager = mockFragmentManager()
            val feature =
                spy(
                    DownloadsFeature(
                        spyContext,
                        store,
                        downloadsUseCases,
                        downloadManager = mock(),
                        shouldForwardToThirdParties = { true },
                        downloadFileUtils = FakeDownloadFileUtils(),
                        fragmentManager = fragmentManager,
                        mainDispatcher = testDispatcher,
                    )
                )
//    @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`() =
//        runTest(testDispatcher) {
//            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 = BrowserStore(),
//                        useCases = usecases,
//                        mainDispatcher = testDispatcher,
//                        downloadFileUtils = FakeDownloadFileUtils(),
//                    )
//                )
//            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`() =
//        runTest(testDispatcher) {
//            val spyContext = spy(testContext)
//            val tab = createTab("https://www.mozilla.org", id = "test-tab")
//            val downloadsUseCases = spy(DownloadsUseCases(store, mock()))
//            val consumeDownloadUseCase = mock<ConsumeDownloadUseCase>()
//            val download = DownloadState(url = "https://www.mozilla.org/file.txt", sessionId = "test-tab")
//            val ourApp =
//                DownloaderApp(
//                    name = "app",
//                    packageName = "thridparty.app",
//                    resolver = mock(),
//                    activityName = "",
//                    url = "",
//                    contentType = null,
//                )
//            val anotherApp = mock<DownloaderApp>()
//            val apps = listOf(ourApp, anotherApp)
//            val dialog = DownloadAppChooserDialog()
//            val fragmentManager: FragmentManager = mockFragmentManager()
//            val feature =
//                spy(
//                    DownloadsFeature(
//                        spyContext,
//                        store,
//                        downloadsUseCases,
//                        downloadManager = mock(),
//                        shouldForwardToThirdParties = { true },
//                        downloadFileUtils = FakeDownloadFileUtils(),
//                        fragmentManager = fragmentManager,
//                        mainDispatcher = testDispatcher,
//                    )
//                )

            doReturn(false).`when`(feature).startDownload(any())
            doReturn(dialog).`when`(fragmentManager).findFragmentByTag(DownloadAppChooserDialog.FRAGMENT_TAG)
+1 −1
Changes for mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -730,7 +730,7 @@ abstract class BaseBrowserFragment :
                        .getBoolean(
                            context.getPreferenceKey(R.string.pref_key_external_download_manager),
                            false,
                        )
                        ) && false
                },
                promptsStyling =
                    DownloadsFeature.PromptsStyling(
+1 −0
Changes for mobile/android/fenix/app/src/main/res/xml/preferences.xml: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -205,6 +205,7 @@
        <androidx.preference.Preference
            android:key="@string/pref_key_downloads"
            app:iconSpaceReserved="false"
            app:isPreferenceVisible="false"
            android:title="@string/preferences_downloads_2" />

        <androidx.preference.Preference