Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
The Tor Project
Applications
android-components
Commits
34035640
Commit
34035640
authored
Oct 14, 2019
by
Sawyer Blatz
Browse files
Closes #4896: Adds download management
parent
84f9a06e
Changes
23
Hide whitespace changes
Inline
Side-by-side
components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt
View file @
34035640
...
...
@@ -184,7 +184,7 @@ sealed class ContentAction : BrowserAction() {
/**
* Removes the [DownloadState] of the [ContentState] with the given [sessionId].
*/
data class
ConsumeDownloadAction
(
val
sessionId
:
String
,
val
downloadId
:
Stri
ng
)
:
ContentAction
()
data class
ConsumeDownloadAction
(
val
sessionId
:
String
,
val
downloadId
:
Lo
ng
)
:
ContentAction
()
/**
* Updates the [HitResult] of the [ContentState] with the given [sessionId].
...
...
components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt
View file @
34035640
...
...
@@ -5,7 +5,7 @@
package
mozilla.components.browser.state.state.content
import
android.os.Environment
import
java.util.UUID
import
kotlin.random.Random
/**
* Value type that represents a download request.
...
...
@@ -16,8 +16,12 @@ import java.util.UUID
* @property contentLength The file size reported by the server.
* @property userAgent The user agent to be used for the download.
* @property destinationDirectory The matching destination directory for this type of download.
* @property filePath The file path the file was saved at.
* @property referrerUrl The site that linked to this download.
* @property skipConfirmation Whether or not the confirmation dialog should be shown before the download begins.
* @property id The unique identifier of this download.
*/
@Suppress
(
"Deprecation"
)
data class
DownloadState
(
val
url
:
String
,
val
fileName
:
String
?
=
null
,
...
...
@@ -25,7 +29,9 @@ data class DownloadState(
val
contentLength
:
Long
?
=
null
,
val
userAgent
:
String
?
=
null
,
val
destinationDirectory
:
String
=
Environment
.
DIRECTORY_DOWNLOADS
,
val
filePath
:
String
=
Environment
.
getExternalStoragePublicDirectory
(
destinationDirectory
).
path
+
"/"
+
fileName
,
val
referrerUrl
:
String
?
=
null
,
val
skipConfirmation
:
Boolean
=
false
,
val
id
:
Stri
ng
=
UUID
.
randomUUID
().
toStri
ng
()
val
id
:
Lo
ng
=
Random
.
nextLo
ng
()
)
components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt
View file @
34035640
...
...
@@ -283,7 +283,7 @@ class ContentActionTest {
@Test
fun
`ConsumeDownloadAction
removes
download`
()
{
val
download
:
DownloadState
=
mock
()
doReturn
(
"
1337
"
).
`when`
(
download
).
id
doReturn
(
1337
L
).
`when`
(
download
).
id
store
.
dispatch
(
ContentAction
.
UpdateDownloadAction
(
tab
.
id
,
download
)
...
...
@@ -292,7 +292,7 @@ class ContentActionTest {
assertEquals
(
download
,
tab
.
content
.
download
)
store
.
dispatch
(
ContentAction
.
ConsumeDownloadAction
(
tab
.
id
,
downloadId
=
"
1337
"
)
ContentAction
.
ConsumeDownloadAction
(
tab
.
id
,
downloadId
=
1337
)
).
joinBlocking
()
assertNull
(
tab
.
content
.
download
)
...
...
@@ -301,7 +301,7 @@ class ContentActionTest {
@Test
fun
`ConsumeDownloadAction
does
not
remove
download
with
different
id`
()
{
val
download
:
DownloadState
=
mock
()
doReturn
(
"
1337
"
).
`when`
(
download
).
id
doReturn
(
1337
L
).
`when`
(
download
).
id
store
.
dispatch
(
ContentAction
.
UpdateDownloadAction
(
tab
.
id
,
download
)
...
...
@@ -310,7 +310,7 @@ class ContentActionTest {
assertEquals
(
download
,
tab
.
content
.
download
)
store
.
dispatch
(
ContentAction
.
ConsumeDownloadAction
(
tab
.
id
,
downloadId
=
"
4223
"
)
ContentAction
.
ConsumeDownloadAction
(
tab
.
id
,
downloadId
=
4223
)
).
joinBlocking
()
assertNotNull
(
tab
.
content
.
download
)
...
...
components/concept/fetch/src/main/java/mozilla/components/concept/fetch/Headers.kt
View file @
34035640
...
...
@@ -51,6 +51,8 @@ interface Headers : Iterable<Header> {
*/
object
Names
{
const
val
CONTENT_DISPOSITION
=
"Content-Disposition"
const
val
CONTENT_RANGE
=
"Content-Range"
const
val
RANGE
=
"Range"
const
val
CONTENT_LENGTH
=
"Content-Length"
const
val
CONTENT_TYPE
=
"Content-Type"
const
val
COOKIE
=
"Cookie"
...
...
components/feature/downloads/build.gradle
View file @
34035640
...
...
@@ -35,6 +35,7 @@ dependencies {
implementation
project
(
':support-ktx'
)
implementation
project
(
':support-base'
)
implementation
project
(
':support-utils'
)
implementation
project
(
':ui-icons'
)
implementation
Dependencies
.
androidx_core_ktx
implementation
Dependencies
.
androidx_localbroadcastmanager
...
...
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/AbstractFetchDownloadService.kt
View file @
34035640
...
...
@@ -7,9 +7,13 @@ package mozilla.components.feature.downloads
import
android.annotation.TargetApi
import
android.app.DownloadManager.ACTION_DOWNLOAD_COMPLETE
import
android.app.DownloadManager.EXTRA_DOWNLOAD_ID
import
android.app.Service
import
android.content.BroadcastReceiver
import
android.content.ContentValues
import
android.content.Context
import
android.content.Intent
import
android.content.Intent.ACTION_VIEW
import
android.content.IntentFilter
import
android.os.Build
import
android.os.Build.VERSION.SDK_INT
import
android.os.Environment
...
...
@@ -18,27 +22,28 @@ import android.os.ParcelFileDescriptor
import
android.provider.MediaStore
import
androidx.annotation.VisibleForTesting
import
androidx.core.app.NotificationManagerCompat
import
androidx.core.content.FileProvider
import
androidx.core.net.toUri
import
androidx.localbroadcastmanager.content.LocalBroadcastManager
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.Dispatchers.IO
import
kotlinx.coroutines.withContext
import
kotlinx.coroutines.Job
import
kotlinx.coroutines.launch
import
mozilla.components.browser.state.state.content.DownloadState
import
mozilla.components.concept.fetch.Client
import
mozilla.components.concept.fetch.Header
import
mozilla.components.concept.fetch.Headers.Names.CONTENT_LENGTH
import
mozilla.components.concept.fetch.Headers.Names.CONTENT_TYPE
import
mozilla.components.concept.fetch.Headers.Names.REFERRER
import
mozilla.components.concept.fetch.Headers.Names.CONTENT_RANGE
import
mozilla.components.concept.fetch.Headers.Names.RANGE
import
mozilla.components.concept.fetch.MutableHeaders
import
mozilla.components.concept.fetch.Request
import
mozilla.components.concept.fetch.toMutableHeaders
import
mozilla.components.feature.downloads.ext.addCompletedDownload
import
mozilla.components.feature.downloads.ext.getDownloadExtra
import
mozilla.components.feature.downloads.ext.withResponse
import
mozilla.components.support.base.ids.NotificationIds
import
mozilla.components.support.base.ids.notify
import
java.io.File
import
java.io.FileOutputStream
import
java.io.IOException
import
java.io.InputStream
import
java.io.OutputStream
import
kotlin.random.Random
/**
* Service that performs downloads through a fetch [Client] rather than through the native
...
...
@@ -46,7 +51,8 @@ import java.io.OutputStream
*
* To use this service, you must create a subclass in your application and it to the manifest.
*/
abstract
class
AbstractFetchDownloadService
:
CoroutineService
()
{
@Suppress
(
"TooManyFunctions"
,
"LargeClass"
)
abstract
class
AbstractFetchDownloadService
:
Service
()
{
protected
abstract
val
httpClient
:
Client
@VisibleForTesting
...
...
@@ -54,55 +60,221 @@ abstract class AbstractFetchDownloadService : CoroutineService() {
@VisibleForTesting
internal
val
context
:
Context
get
()
=
this
override
fun
onCreate
()
{
startForeground
(
NotificationIds
.
getIdForTag
(
context
,
ONGOING_DOWNLOAD_NOTIFICATION_TAG
),
DownloadNotification
.
createOngoingDownloadNotification
(
context
)
)
super
.
onCreate
()
internal
var
downloadJobs
=
mutableMapOf
<
Long
,
DownloadJobState
>()
internal
data class
DownloadJobState
(
var
job
:
Job
?
=
null
,
var
state
:
DownloadState
,
var
currentBytesCopied
:
Long
=
0
,
var
status
:
DownloadJobStatus
,
var
foregroundServiceId
:
Int
=
0
)
internal
enum
class
DownloadJobStatus
{
ACTIVE
,
PAUSED
,
CANCELLED
,
FAILED
}
internal
val
broadcastReceiver
by
lazy
{
object
:
BroadcastReceiver
()
{
override
fun
onReceive
(
context
:
Context
,
intent
:
Intent
?)
{
val
downloadId
=
intent
?.
extras
?.
getLong
(
DownloadNotification
.
EXTRA_DOWNLOAD_ID
)
?:
return
val
currentDownloadJobState
=
downloadJobs
[
downloadId
]
?:
return
when
(
intent
.
action
)
{
ACTION_PAUSE
->
{
currentDownloadJobState
.
status
=
DownloadJobStatus
.
PAUSED
currentDownloadJobState
.
job
?.
cancel
()
}
ACTION_RESUME
->
{
NotificationManagerCompat
.
from
(
context
).
cancel
(
currentDownloadJobState
.
foregroundServiceId
)
currentDownloadJobState
.
status
=
DownloadJobStatus
.
ACTIVE
currentDownloadJobState
.
job
=
CoroutineScope
(
IO
).
launch
{
startDownloadJob
(
currentDownloadJobState
.
state
)
}
}
ACTION_CANCEL
->
{
currentDownloadJobState
.
status
=
DownloadJobStatus
.
CANCELLED
stopForeground
(
true
)
currentDownloadJobState
.
job
?.
cancel
()
}
ACTION_TRY_AGAIN
->
{
NotificationManagerCompat
.
from
(
context
).
cancel
(
currentDownloadJobState
.
foregroundServiceId
)
currentDownloadJobState
.
status
=
DownloadJobStatus
.
ACTIVE
currentDownloadJobState
.
job
=
CoroutineScope
(
IO
).
launch
{
startDownloadJob
(
currentDownloadJobState
.
state
)
}
}
ACTION_OPEN
->
{
// Create a new file with the location of the saved file to extract the correct path
// `file` has the wrong path, so we must construct it based on the `fileName` and `dir.path`s
val
fileLocation
=
File
(
currentDownloadJobState
.
state
.
filePath
)
val
filePath
=
FileProvider
.
getUriForFile
(
context
,
context
.
packageName
+
FILE_PROVIDER_EXTENSION
,
fileLocation
)
val
newIntent
=
Intent
(
ACTION_VIEW
).
apply
{
setDataAndType
(
filePath
,
currentDownloadJobState
.
state
.
contentType
?:
"*/*"
)
flags
=
Intent
.
FLAG_ACTIVITY_NEW_TASK
or
Intent
.
FLAG_GRANT_READ_URI_PERMISSION
}
startActivity
(
newIntent
)
}
}
}
}
}
override
fun
onBind
(
intent
:
Intent
?):
IBinder
?
=
null
override
suspend
fun
onStartCommand
(
intent
:
Intent
?,
flags
:
Int
)
{
val
download
=
intent
?.
getDownloadExtra
()
?:
return
override
fun
onStartCommand
(
intent
:
Intent
?,
flags
:
Int
,
startId
:
Int
):
Int
{
val
download
=
intent
?.
getDownloadExtra
()
?:
return
START_REDELIVER_INTENT
registerForUpdates
()
val
foregroundServiceId
=
Random
.
nextInt
()
// Create a new job and add it, with its downloadState to the map
downloadJobs
[
download
.
id
]
=
DownloadJobState
(
state
=
download
,
foregroundServiceId
=
foregroundServiceId
,
status
=
DownloadJobStatus
.
ACTIVE
)
downloadJobs
[
download
.
id
]
?.
job
=
CoroutineScope
(
IO
).
launch
{
startDownloadJob
(
download
)
}
return
super
.
onStartCommand
(
intent
,
flags
,
startId
)
}
override
fun
onDestroy
()
{
super
.
onDestroy
()
downloadJobs
.
values
.
forEach
{
it
.
job
?.
cancel
()
}
}
internal
fun
startDownloadJob
(
download
:
DownloadState
)
{
val
notification
=
try
{
performDownload
(
download
)
DownloadNotification
.
createDownloadCompletedNotification
(
context
,
download
.
fileName
)
when
(
downloadJobs
[
download
.
id
]
?.
status
)
{
DownloadJobStatus
.
CANCELLED
->
{
return
}
DownloadJobStatus
.
PAUSED
->
{
DownloadNotification
.
createPausedDownloadNotification
(
context
,
download
)
}
DownloadJobStatus
.
ACTIVE
->
{
DownloadNotification
.
createDownloadCompletedNotification
(
context
,
download
)
}
DownloadJobStatus
.
FAILED
->
{
DownloadNotification
.
createDownloadFailedNotification
(
context
,
download
)
}
null
->
{
return
}
}
}
catch
(
e
:
IOException
)
{
DownloadNotification
.
createDownloadFailedNotification
(
context
,
download
.
fileName
)
DownloadNotification
.
createDownloadFailedNotification
(
context
,
download
)
}
NotificationManagerCompat
.
from
(
context
).
notify
(
downloadJobs
[
download
.
id
]
?.
foregroundServiceId
?:
0
,
notification
)
sendDownloadCompleteBroadcast
(
download
.
id
)
}
private
fun
registerForUpdates
()
{
val
filter
=
IntentFilter
().
apply
{
addAction
(
ACTION_PAUSE
)
addAction
(
ACTION_RESUME
)
addAction
(
ACTION_CANCEL
)
addAction
(
ACTION_TRY_AGAIN
)
addAction
(
ACTION_OPEN
)
}
context
.
registerReceiver
(
broadcastReceiver
,
filter
)
}
private
fun
displayOngoingDownloadNotification
(
download
:
DownloadState
)
{
val
ongoingDownloadNotification
=
DownloadNotification
.
createOngoingDownloadNotification
(
context
,
COMPLETED_DOWNLOAD_NOTIFICATION_TAG
,
notification
download
)
val
downloadID
=
intent
.
getLongExtra
(
EXTRA_DOWNLOAD_ID
,
-
1
)
sendDownloadCompleteBroadcast
(
downloadID
)
// We want to startForeground so that the system is less likely to kill our service under memory pressure.
startForeground
(
downloadJobs
[
download
.
id
]
?.
foregroundServiceId
?:
0
,
ongoingDownloadNotification
)
}
private
suspend
fun
performDownload
(
download
:
DownloadState
)
=
withContext
(
IO
)
{
val
headers
=
listOf
(
CONTENT_TYPE
to
download
.
contentType
,
CONTENT_LENGTH
to
download
.
contentLength
?.
toString
(),
REFERRER
to
download
.
referrerUrl
).
mapNotNull
{
(
name
,
value
)
->
if
(
value
.
isNullOrBlank
())
null
else
Header
(
name
,
value
)
}.
toMutableHeaders
()
@Suppress
(
"ComplexCondition"
)
internal
fun
performDownload
(
download
:
DownloadState
)
{
val
isResumingDownload
=
downloadJobs
[
download
.
id
]
?.
currentBytesCopied
?:
0L
>
0L
val
headers
=
MutableHeaders
()
val
request
=
Request
(
download
.
url
,
headers
=
headers
)
if
(
isResumingDownload
)
{
headers
.
append
(
RANGE
,
"bytes=${downloadJobs[download.id]?.currentBytesCopied}-"
)
}
val
request
=
Request
(
download
.
url
,
headers
=
headers
)
val
response
=
httpClient
.
fetch
(
request
)
// If we are resuming a download and the response does not contain a CONTENT_RANGE
// we cannot be sure that the request will properly be handled
if
(
response
.
status
!=
PARTIAL_CONTENT_STATUS
&&
response
.
status
!=
OK_STATUS
||
(
isResumingDownload
&&
!
response
.
headers
.
contains
(
CONTENT_RANGE
)))
{
// We experienced a problem trying to fetch the file, send a failure notification
downloadJobs
[
download
.
id
]
?.
currentBytesCopied
=
0
downloadJobs
[
download
.
id
]
?.
status
=
DownloadJobStatus
.
FAILED
return
}
response
.
body
.
useStream
{
inStream
->
useFileStream
(
download
.
withResponse
(
response
.
headers
,
inStream
))
{
outStream
->
inStream
.
copyTo
(
outStream
)
val
newDownloadState
=
download
.
withResponse
(
response
.
headers
,
inStream
)
downloadJobs
[
download
.
id
]
?.
state
=
newDownloadState
displayOngoingDownloadNotification
(
newDownloadState
)
useFileStream
(
newDownloadState
,
isResumingDownload
)
{
outStream
->
copyInChunks
(
downloadJobs
[
download
.
id
]
!!
,
inStream
,
outStream
)
}
}
}
private
fun
copyInChunks
(
downloadJobState
:
DownloadJobState
,
inStream
:
InputStream
,
outStream
:
OutputStream
)
{
// To ensure that we copy all files (even ones that don't have fileSize, we must NOT check < fileSize
while
(
downloadJobState
.
status
==
DownloadJobStatus
.
ACTIVE
)
{
val
data
=
ByteArray
(
CHUNK_SIZE
)
val
bytesRead
=
inStream
.
read
(
data
)
// If bytesRead is -1, there's no data left to read from the stream
if
(
bytesRead
==
-
1
)
{
break
}
downloadJobState
.
currentBytesCopied
+=
bytesRead
outStream
.
write
(
data
,
0
,
bytesRead
)
}
}
/**
* Informs [mozilla.components.feature.downloads.manager.FetchDownloadManager] that a download
* has been completed.
...
...
@@ -121,12 +293,13 @@ abstract class AbstractFetchDownloadService : CoroutineService() {
*/
internal
fun
useFileStream
(
download
:
DownloadState
,
append
:
Boolean
,
block
:
(
OutputStream
)
->
Unit
)
{
if
(
SDK_INT
>=
Build
.
VERSION_CODES
.
Q
)
{
useFileStreamScopedStorage
(
download
,
block
)
}
else
{
useFileStreamLegacy
(
download
,
block
)
useFileStreamLegacy
(
download
,
append
,
block
)
}
}
...
...
@@ -153,10 +326,11 @@ abstract class AbstractFetchDownloadService : CoroutineService() {
@TargetApi
(
Build
.
VERSION_CODES
.
P
)
@Suppress
(
"Deprecation"
)
private
fun
useFileStreamLegacy
(
download
:
DownloadState
,
block
:
(
OutputStream
)
->
Unit
)
{
private
fun
useFileStreamLegacy
(
download
:
DownloadState
,
append
:
Boolean
,
block
:
(
OutputStream
)
->
Unit
)
{
val
dir
=
Environment
.
getExternalStoragePublicDirectory
(
download
.
destinationDirectory
)
val
file
=
File
(
dir
,
download
.
fileName
!!
)
FileOutputStream
(
file
).
use
(
block
)
FileOutputStream
(
file
,
append
).
use
(
block
)
addCompletedDownload
(
title
=
download
.
fileName
!!
,
...
...
@@ -173,7 +347,15 @@ abstract class AbstractFetchDownloadService : CoroutineService() {
}
companion
object
{
private
const
val
ONGOING_DOWNLOAD_NOTIFICATION_TAG
=
"OngoingDownload"
private
const
val
COMPLETED_DOWNLOAD_NOTIFICATION_TAG
=
"CompletedDownload"
private
const
val
FILE_PROVIDER_EXTENSION
=
".fileprovider"
private
const
val
CHUNK_SIZE
=
4
*
1024
private
const
val
PARTIAL_CONTENT_STATUS
=
206
private
const
val
OK_STATUS
=
200
const
val
ACTION_OPEN
=
"mozilla.components.feature.downloads.OPEN"
const
val
ACTION_PAUSE
=
"mozilla.components.feature.downloads.PAUSE"
const
val
ACTION_RESUME
=
"mozilla.components.feature.downloads.RESUME"
const
val
ACTION_CANCEL
=
"mozilla.components.feature.downloads.CANCEL"
const
val
ACTION_TRY_AGAIN
=
"mozilla.components.feature.downloads.TRY_AGAIN"
}
}
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/CoroutineService.kt
deleted
100644 → 0
View file @
84f9a06e
/* 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
import
android.app.Service
import
android.content.Intent
import
androidx.annotation.CallSuper
import
androidx.annotation.VisibleForTesting
import
androidx.annotation.VisibleForTesting.PROTECTED
import
kotlinx.coroutines.CoroutineDispatcher
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.Job
import
kotlinx.coroutines.cancel
import
kotlinx.coroutines.launch
/**
* Service that runs suspend functions in parallel.
* When all jobs are completed, the service is stopped automatically.
*/
abstract
class
CoroutineService
(
jobDispatcher
:
CoroutineDispatcher
=
Dispatchers
.
IO
)
:
Service
()
{
private
val
scope
=
CoroutineScope
(
jobDispatcher
)
private
val
runningJobs
=
mutableSetOf
<
Job
>()
/**
* Called by every time a client explicitly starts the service by calling
* [android.content.Context.startService], providing the arguments it supplied.
* Do not call this method directly.
*
* @param intent The Intent supplied to [android.content.Context.startService], as given.
* This may be null if the service is being restarted after its process has gone away.
* @param flags Additional data about this start request.
*/
@VisibleForTesting
(
otherwise
=
PROTECTED
)
internal
abstract
suspend
fun
onStartCommand
(
intent
:
Intent
?,
flags
:
Int
)
/**
* Starts a job using [onStartCommand] then stops the service once all jobs are complete.
*/
final
override
fun
onStartCommand
(
intent
:
Intent
?,
flags
:
Int
,
startId
:
Int
):
Int
{
val
job
=
scope
.
launch
{
onStartCommand
(
intent
,
flags
)
}
synchronized
(
runningJobs
)
{
runningJobs
.
add
(
job
)
}
job
.
invokeOnCompletion
{
cleanupJob
(
job
)
}
return
START_REDELIVER_INTENT
}
/**
* Stops all jobs when the service is destroyed.
*/
@CallSuper
override
fun
onDestroy
()
{
scope
.
cancel
()
}
private
fun
cleanupJob
(
job
:
Job
)
=
synchronized
(
runningJobs
)
{
runningJobs
.
remove
(
job
)
if
(
runningJobs
.
isEmpty
())
{
stopSelf
()
}
}
}
components/feature/downloads/src/main/java/mozilla/components/feature/downloads/DownloadDialogFragment.kt
View file @
34035640
...
...
@@ -5,8 +5,9 @@
package
mozilla.components.feature.downloads
import
android.os.Bundle
import
androidx.
fragment.app.
DialogFragment
import
androidx.
appcompat.app.AppCompat
DialogFragment
import
mozilla.components.browser.state.state.content.DownloadState
import
mozilla.components.feature.downloads.DownloadDialogFragment.Companion.MEGABYTE
import
mozilla.components.support.utils.DownloadUtils
/**
...
...
@@ -15,7 +16,7 @@ import mozilla.components.support.utils.DownloadUtils
* If [SimpleDownloadDialogFragment] is not flexible enough for your use case you should inherit for this class.
* Be mindful to call [onStartDownload] when you want to start the download.
*/
abstract
class
DownloadDialogFragment
:
DialogFragment
()
{
abstract
class
DownloadDialogFragment
:
AppCompat
DialogFragment
()
{
/**
* A callback to trigger a download, call it when you are ready to start a download. For instance,
...
...
@@ -27,7 +28,7 @@ abstract class DownloadDialogFragment : DialogFragment() {
var
onCancelDownload
:
()
->
Unit
=
{}
/**
*
a
dd the metadata of this download object to the arguments of this fragment.
*
A
dd the metadata of this download object to the arguments of this fragment.
*/
fun
setDownload
(
download
:
DownloadState
)
{
val
args
=
arguments
?:
Bundle
()
...
...
@@ -53,5 +54,11 @@ abstract class DownloadDialogFragment : DialogFragment() {
const
val
KEY_URL
=
"KEY_URL"
const
val
FRAGMENT_TAG
=
"SHOULD_DOWNLOAD_PROMPT_DIALOG"
const
val
MEGABYTE
=
1024.0
*
1024.0
}
}