Skip to content

Commit

Permalink
Issue mozilla-mobile#1968 - Add FetchDownloadManager
Browse files Browse the repository at this point in the history
  • Loading branch information
NotWoods committed Jun 25, 2019
1 parent c1ccc76 commit 5625c1a
Show file tree
Hide file tree
Showing 15 changed files with 323 additions and 135 deletions.
1 change: 0 additions & 1 deletion components/browser/session/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ dependencies {
api project(':browser-state')

implementation project(':concept-engine')
implementation project(':concept-fetch')
implementation project(':support-utils')
implementation project(':support-ktx')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package mozilla.components.browser.session

import android.os.Environment
import mozilla.components.concept.fetch.Response

/**
* Value type that represents a Download.
Expand All @@ -23,6 +22,5 @@ data class Download(
val contentType: String? = null,
val contentLength: Long? = null,
val userAgent: String? = null,
val destinationDirectory: String = Environment.DIRECTORY_DOWNLOADS,
val response: Response? = null
val destinationDirectory: String = Environment.DIRECTORY_DOWNLOADS
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

package mozilla.components.concept.fetch

import java.lang.IllegalArgumentException

/**
* A collection of HTTP [Headers] (immutable) of a [Request] or [Response].
*/
Expand Down Expand Up @@ -53,6 +51,7 @@ interface Headers : Iterable<Header> {
*/
object Names {
const val CONTENT_TYPE = "Content-Type"
const val CONTENT_LENGTH = "Content-Length"
const val USER_AGENT = "User-Agent"
}

Expand Down
2 changes: 2 additions & 0 deletions components/feature/downloads/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ dependencies {
implementation project(':support-utils')

implementation Dependencies.androidx_core_ktx
implementation Dependencies.kotlin_coroutines
implementation Dependencies.kotlin_stdlib

testImplementation Dependencies.androidx_test_core
testImplementation Dependencies.androidx_test_junit
testImplementation Dependencies.kotlin_coroutines_test
testImplementation Dependencies.testing_robolectric
testImplementation Dependencies.testing_mockito
testImplementation project(':support-test')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import android.Manifest.permission.INTERNET
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.annotation.SuppressLint
import android.content.Context
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import mozilla.components.browser.session.Download
import mozilla.components.browser.session.SelectionAwareSessionObserver
import mozilla.components.browser.session.Session
Expand All @@ -21,6 +26,7 @@ import mozilla.components.feature.downloads.manager.DownloadManager
import mozilla.components.feature.downloads.manager.OnDownloadCompleted
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.observer.Consumable
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.support.ktx.android.content.isPermissionGranted

typealias OnNeedToRequestPermissions = (permissions: Array<String>) -> Unit
Expand All @@ -46,14 +52,14 @@ typealias OnNeedToRequestPermissions = (permissions: Array<String>) -> Unit
class DownloadsFeature(
private val applicationContext: Context,
var onNeedToRequestPermissions: OnNeedToRequestPermissions = { },
var onDownloadCompleted: OnDownloadCompleted = { _, _ -> },
private val downloadManager: DownloadManager = AndroidDownloadManager(applicationContext, onDownloadCompleted),
var onDownloadCompleted: OnDownloadCompleted = { },
private val downloadManager: DownloadManager = AndroidDownloadManager(applicationContext),
sessionManager: SessionManager,
private val sessionId: String? = null,
private val fragmentManager: FragmentManager? = null,
@VisibleForTesting(otherwise = PRIVATE)
internal var dialog: DownloadDialogFragment = SimpleDownloadDialogFragment.newInstance()
) : SelectionAwareSessionObserver(sessionManager), LifecycleAwareFeature {
) : SelectionAwareSessionObserver(sessionManager), LifecycleAwareFeature, CoroutineScope by MainScope() {

/**
* Starts observing downloads on the selected session and sends them to the [DownloadManager]
Expand All @@ -72,7 +78,8 @@ class DownloadsFeature(
*/
override fun stop() {
super.stop()
downloadManager.unregisterListener()
cancel()
downloadManager.unregisterListeners()
}

/**
Expand All @@ -85,15 +92,26 @@ class DownloadsFeature(
showDialog(download, session)
false
} else {
downloadManager.download(download)
true
startDownload(download)
}
} else {
onNeedToRequestPermissions(arrayOf(INTERNET, WRITE_EXTERNAL_STORAGE))
false
}
}

private fun startDownload(download: Download): Boolean {
launch {
val completed = downloadManager.download(download)
if (completed != null) {
onDownloadCompleted(completed)
} else {
showUnSupportFileErrorMessage()
}
}
return true
}

/**
* Notifies the feature that the permissions request was completed. It will then
* either trigger or clear the pending download.
Expand All @@ -104,22 +122,28 @@ class DownloadsFeature(
) {
if (applicationContext.isPermissionGranted(INTERNET, WRITE_EXTERNAL_STORAGE)) {
activeSession?.let { session ->
session.download.consume {
onDownload(session, it)
}
session.download.consume { onDownload(session, it) }
}
} else {
activeSession?.download = Consumable.empty()
}
}

private fun showUnSupportFileErrorMessage() {
val text = applicationContext.getString(
R.string.mozac_feature_downloads_file_not_supported2,
applicationContext.appName
)

Toast.makeText(applicationContext, text, Toast.LENGTH_LONG).show()
}

@SuppressLint("MissingPermission")
private fun showDialog(download: Download, session: Session) {
dialog.setDownload(download)

dialog.onStartDownload = {
downloadManager.download(download)
session.download.consume { true }
session.download.consume(this::startDownload)
}

if (!isAlreadyADialogCreated()) {
Expand All @@ -132,13 +156,10 @@ class DownloadsFeature(
}

private fun reAttachOnStartDownloadListener(previousDialog: DownloadDialogFragment?) {
previousDialog?.apply {
this@DownloadsFeature.dialog = this
previousDialog?.let {
dialog = it
activeSession?.let { session ->
session.download.consume {
onDownload(session, it)
false
}
session.download.consume { download -> onDownload(session, download) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,30 @@ import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.util.LongSparseArray
import android.widget.Toast
import androidx.annotation.RequiresPermission
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.core.util.contains
import androidx.core.util.forEach
import androidx.core.util.isEmpty
import androidx.core.util.set
import kotlinx.coroutines.CompletableDeferred
import mozilla.components.browser.session.Download
import mozilla.components.feature.downloads.R
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.support.ktx.android.content.isPermissionGranted
import mozilla.components.support.utils.DownloadUtils

typealias SystemDownloadManager = android.app.DownloadManager
typealias SystemRequest = android.app.DownloadManager.Request

/**
* Handles the interactions with the [AndroidDownloadManager].
* @property onDownloadCompleted a callback to be notified when a download is completed.
* @property applicationContext a reference to [Context] applicationContext.
*/
class AndroidDownloadManager(
private val applicationContext: Context,
override var onDownloadCompleted: OnDownloadCompleted = { _, _ -> }
private val applicationContext: Context
) : DownloadManager {

private val queuedDownloads = LongSparseArray<Download>()
private val queuedJobs = LongSparseArray<CompletableDeferred<AndroidCompletedDownload>>()
private var isSubscribedReceiver = false
private lateinit var androidDownloadManager: SystemDownloadManager

/**
* Schedule a download through the [AndroidDownloadManager].
Expand All @@ -53,27 +48,26 @@ class AndroidDownloadManager(
* @return the id reference of the scheduled download.
*/
@RequiresPermission(allOf = [INTERNET, WRITE_EXTERNAL_STORAGE])
override fun download(
override suspend fun download(
download: Download,
refererUrl: String,
cookie: String
): Long {
): AndroidCompletedDownload? {

val androidDownloadManager: SystemDownloadManager = applicationContext.getSystemService()!!

if (download.isSupportedProtocol()) {
// We are ignoring everything that is not http or https. This is a limitation of
// Android's download manager. There's no reason to show a download dialog for
// something we can't download anyways.
showUnSupportFileErrorMessage()
return FILE_NOT_SUPPORTED
return null
}

if (!applicationContext.isPermissionGranted(INTERNET, WRITE_EXTERNAL_STORAGE)) {
throw SecurityException("You must be granted INTERNET and WRITE_EXTERNAL_STORAGE permissions")
}

androidDownloadManager = applicationContext.getSystemService()!!

val fileName = getFileName(download)
val fileName = download.getFileName()

val request = SystemRequest(download.url.toUri())
.setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
Expand All @@ -91,65 +85,53 @@ class AndroidDownloadManager(
request.setDestinationInExternalPublicDir(download.destinationDirectory, fileName)

val downloadID = androidDownloadManager.enqueue(request)
queuedDownloads[downloadID] = download
queuedJobs[downloadID] = CompletableDeferred()

if (!isSubscribedReceiver) {
registerBroadcastReceiver(applicationContext)
}

return downloadID
return queuedJobs[downloadID].await()
}

/**
* Remove all the listeners.
*/
override fun unregisterListener() {
override fun unregisterListeners() {
if (isSubscribedReceiver) {
applicationContext.unregisterReceiver(onDownloadComplete)
queuedJobs.apply {
forEach { _, context -> context.cancel() }
clear()
}
isSubscribedReceiver = false
queuedDownloads.clear()
}
}

private fun registerBroadcastReceiver(context: Context) {
val filter = IntentFilter(ACTION_DOWNLOAD_COMPLETE)
context.registerReceiver(onDownloadComplete, filter)
isSubscribedReceiver = true
}

private fun getFileName(download: Download): String? {
return if (download.fileName.isNotEmpty()) {
download.fileName
} else {
DownloadUtils.guessFileName(
"",
download.url,
download.contentType
)
if (!isSubscribedReceiver) {
val filter = IntentFilter(ACTION_DOWNLOAD_COMPLETE)
context.registerReceiver(onDownloadComplete, filter)
isSubscribedReceiver = true
}
}

private val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
if (queuedDownloads.isEmpty()) {
unregisterListener()
}
if (queuedJobs.isEmpty()) unregisterListeners()

val downloadID = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1)

if (downloadID in queuedDownloads) {
val download = queuedDownloads[downloadID]
if (downloadID in queuedJobs) {
val deferred = queuedJobs[downloadID]

download?.let {
onDownloadCompleted.invoke(download, downloadID)
}
queuedDownloads.remove(downloadID)
deferred?.complete(AndroidCompletedDownload(id = downloadID))

if (queuedDownloads.isEmpty()) {
unregisterListener()
}
queuedJobs.remove(downloadID)
}

if (queuedJobs.isEmpty()) unregisterListeners()
}
}

Expand All @@ -158,14 +140,9 @@ class AndroidDownloadManager(
return (scheme == null || scheme != "http" && scheme != "https")
}

private fun showUnSupportFileErrorMessage() {
val text = applicationContext.getString(
R.string.mozac_feature_downloads_file_not_supported2,
applicationContext.appName)

Toast.makeText(applicationContext, text, Toast.LENGTH_LONG)
.show()
}
data class AndroidCompletedDownload(
val id: Long
) : CompletedDownload
}

internal fun SystemRequest.addRequestHeaderSafely(name: String, value: String?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.downloads.manager

interface CompletedDownload

typealias OnDownloadCompleted = (CompletedDownload) -> Unit
Loading

0 comments on commit 5625c1a

Please sign in to comment.