What I need to do:
Download a file by URL using DownloadManager, and separetely handle successful download and error download.
What I tried:
I used BroadcastReceiver to catch the result of file download. I tried to use DownloadManager.ACTION_DOWNLOAD_COMPLETE as a marker but it fires not only when file is successfully downloaded but also when error occured and no file was downloaded.
So it seems like DownloadManager.ACTION_DOWNLOAD_COMPLETE reports only that attempt to download was made no matter with what result.
Is there a way to catch only successful downloads?
my code:
fragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
downloadCompleteReceiver = object : BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
Snackbar.make(requireActivity().findViewById(android.R.id.content), getString(R.string.alert_files_successfully_downloaded), Snackbar.LENGTH_LONG).show()
}
}
val filter = IntentFilter()
filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
requireActivity().registerReceiver(downloadCompleteReceiver, filter)
}
Request:
fun downloadMediaFiles(listOfUrls: List<MediaDto>, activity: Activity, authToken:String) {
if (isPermissionStorageProvided(activity)) {
val manager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
listOfUrls.forEach {
val request = DownloadManager.Request(Uri.parse(it.url))
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
request.setTitle(activity.getString(R.string.download_manager_title))
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
request.setDestinationInExternalPublicDir(
getDestinationDirectoryFromFileExtension(it.url),
"${System.currentTimeMillis()}"
)
request.addRequestHeader("authorization", authToken)
manager.enqueue(request)
}
}
}
SOLVED
What Rediska wrote + need also add this to my BroadcastReceiver object:
val referenceId = intent!!.getLongExtra(
DownloadManager.EXTRA_DOWNLOAD_ID, -1L
)
and then pass this referenceId to getDownloadStatus as an arguement.
getDownloadStatus returns integer of 8 when successfull and 16 if failure, which I can further process.
This function will return the status of the download. See DownloadManager for values. It returns -1 if the download not found for given id.
int getDownloadStatus(long id) {
try {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(id);
DownloadManager downloadManager = (DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE);
Cursor cursor = downloadManager.query(query);
if (cursor.getCount() == 0) return -1;
cursor.moveToFirst();
return cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
} catch(Exception ex) { return -1; }
}
Related
I am trying to download a file into the external public directory but the Uri returned by downloadManager.getUriForDownloadedFile(requestId) isn't usable. I'm unable to launch an ACTION_OPEN intent with it, even though this same process works for Android 10.
I suspect this has something to do with missing updated permissions on Android 13, but there are no errors logged in logcat.
I am able to get it working as expected by using setDestinationInExternalFilesDir to store the file inside the private applications directory and using a ContentResolver to copy it into the phones external media storage, but that is a lot of code and very verbose. Using setDestinationInExternalPublicDir from DownloadManager is a lot cleaner and concise.
This is how I am creating and enqueuing my request
val request = DownloadManager
.Request(Uri.parse(downloadUrl))
.setDestinationInExternalPublicDir(
Environment.DIRECTORY_MOVIES,
"${UUID.randomUUID()}.mp4"
)
.setMimeType("video/mp4")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setTitle("Saving...")
.setRequiresCharging(false)
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)
requestId = downloadManager.enqueue(request)
And this is how I am listening for download completion and attempting to use the Uri.
private val downloadBroadCastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val requestId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1
val query = DownloadManager.Query()
query.setFilterById(requestId)
val cursor = downloadManager.query(query)
if (cursor.moveToFirst()) {
val columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)
when (cursor.getInt(columnIndex)) {
DownloadManager.STATUS_SUCCESSFUL -> {
LOG.info("onReceive: Video download completed!")
val uri = downloadManager.getUriForDownloadedFile(requestId)
context.startActivity(
Intent(Intent.ACTION_VIEW, uri).apply {
setDataAndType(uri, "video/mp4")
}
)
}
}
}
}
I have an implementation of DownloadManager that works correctly on most devices I've tested it. I recently started testing on a Samsung Galaxy S10 (Android 9), and I noticed a completely different behavior. the queued download takes up to 10 min to even start, I can see this because my download's request visibility is VISIBILITY_VISIBLE_NOTIFY_COMPLETED so it shows up as a notification after several min of the download request being queued.
When the download completes (either failing or succeeding) I also noticed that I don't get a call to my registered BroadcastReceiver until probably other 10 min, I know iths finished because, it's visible on the notifications section of the OS.
Has anyone faced this or know how to make DownloadManager behave as its expected?
I've considered rewriting the component so I don't depend on DownloadManager but that exactly what Im trying to avoid.
This is my implementation:
Permissions:
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE " />
DownloadManager implementation
/**
* Manages all DB retrieval and file level validation.
*
*/
class DataBaseInitializationRepository(private val app: Application, private val dbHelper: DatabaseFileHelper) {
fun initiateDB( callback: DownloadCompleteCallback){
val dbUrl = dbHelper.getDbUrl()
val tempDbFile = dbHelper.getTempDbFile()
val permanentDbFile = dbHelper.getPermanentDbFile()
if (!permanentDbFile.exists() && tempDbFile.length() <= 0) {
val downloadManager = app.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downLoadDBRequest = DownloadManager.Request(Uri.parse( dbUrl ))
.setTitle( app.getString( R.string.download_db_title ) )
.setDescription(app.getString( R.string.download_db_description ))
.setDestinationInExternalFilesDir( app,
null,
tempDbFile.path
)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val downloadId = downloadManager.enqueue( downLoadDBRequest )
registerReceiver( callback, downloadId )
}else{
callback.onComplete(0L)
}
}
private fun registerReceiver(callback: DownloadCompleteCallback, downloadId: Long){
val receiver = object: BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id == downloadId){
//Move index reads to reusable function
val downloadManager = app.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query()
query.setFilterById( downloadId )
val data = downloadManager.query( query )
if(data.moveToFirst() && data.count > 0){
val statusIndex = data.getColumnIndex(DownloadManager.COLUMN_STATUS)
val status = data.getInt( statusIndex )
if(status == DownloadManager.STATUS_SUCCESSFUL){
val localUriIndex = data.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
val localFile = File(
data.getString( localUriIndex )
.replace("file://","" )
)
if(localFile.exists()){
permanentlyStoreDb(localFile)
callback.onComplete(id)
}else{
callback.onFail("Initial Database Download Failed - File not found")
}
}else if(status == DownloadManager.STATUS_FAILED){
val reasonIndex = data.getColumnIndex(DownloadManager.COLUMN_REASON)
val reason = data.getInt( reasonIndex )
if(reason == DownloadManager.ERROR_FILE_ALREADY_EXISTS){
callback.onComplete(id)
}else{
callback.onFail("Initial Database Download Failed: $reason")
}
}
}else{
callback.onFail("Initial Database Download Failed - Unable to read download metadata")
}
}
}
}
app.registerReceiver( receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) )
}
private fun permanentlyStoreDb(tempFile: File): File {
val permanentDbFile = dbHelper.getPermanentDbFile()
try {
if(tempFile.exists()) {
tempFile.copyTo(
permanentDbFile,
true,
1024
)
tempFile.delete()
}else{
throw IOException("Temporal DB file doesn't exist")
}
}catch (ioex: IOException) {
throw IOException("Unable to copy DB to permanent storage:", ioex)
}
return permanentDbFile
}
/**
* Allows download completion to be notified back to the calling view model
*/
interface DownloadCompleteCallback{
fun onComplete(downloadId: Long)
fun onFail(message: String)
}
}
DatabaseFileHelper contains the logic to determine the temporary file, the permanent DB location and the DB URL where the download will happen. This is the logic I used for the temporary file:
fun getTempDbFile(): File {
return File.createTempFile(<FILE-LOCATION>, null, app.cacheDir)
}
What I'm trying to do is have a basic pdf downloaded whenever I press a button. When I press the button I'm getting the text indicator "done" but nothing is being downloaded
btn_download.setOnClickListener {
var downloadRequest = DownloadManager.Request(
Uri.parse ("http://www.africau.edu/images/default/sample.pdf"))
.setTitle("a pdf")
.setDescription("a pdf")
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
//over data
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
downloadID = downloadManager.enqueue(downloadRequest)
}
var receiver = object:BroadcastReceiver(){
override fun onReceive(context: Context?, intent: Intent?) {
var id= intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (downloadID == id){
Toast.makeText(applicationContext, "done",Toast.LENGTH_LONG).show()
}
}
}
registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
I am calling below function to download a binary file.
fun downloadFile(
baseActivity: Context,
batteryId: String,
downloadFileUrl: String?,
title: String?
): Long {
val directory =
File(Environment.getExternalStorageDirectory().toString() + "/destination_folder")
if (!directory.exists()) {
directory.mkdirs()
}
//Getting file extension i.e. .bin, .mp4 , .jpg, .png etc..
val fileExtension = downloadFileUrl?.substring(downloadFileUrl.lastIndexOf("."))
val downloadReference: Long
var objDownloadManager: DownloadManager =
baseActivity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val uri = Uri.parse(downloadFileUrl)
val request = DownloadManager.Request(uri)
//Firmware file name as batteryId and extension
firmwareFileSubPath = batteryId + fileExtension
request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
"" + batteryId + fileExtension
)
request.setTitle(title)
downloadReference = objDownloadManager.enqueue(request) ?: 0
return downloadReference
}
Once the file got downloaded I am receiving it in below onReceive() method of Broadcast receiver:
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
intent.extras?.let {
//retrieving the file
val downloadedFileId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID)
val downloadManager =
getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val uri: Uri = downloadManager.getUriForDownloadedFile(downloadedFileId)
viewModel.updateFirmwareFilePathToFirmwareTable(uri)
}
}
}
I am downloading the files one by one and wants to know that which file is downloaded.
Based on the particular file download, I have to update the entry in my local database.
So, here in onReceive() method how can I identify that which specific file is downloaded?
Thanks.
One way to identify your multiple downloads simultaneously is to track id returned from DownloadManager to your local db mapped to given entry when you call objDownloadManager.enqueue(request).
Document of DownloadManager.enquque indicates that:
Enqueue a new download. The download will start automatically once the download manager is ready to execute it and connectivity is available.
So, if you store that id mapped to your local database entry for given record then during onReceive() you can identify back to given record.
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) {
intent.extras?.let {
//retrieving the file
val downloadedFileId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID)
// Find same id from db that you stored previously
val downloadManager =
getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val uri: Uri = downloadManager.getUriForDownloadedFile(downloadedFileId)
viewModel.updateFirmwareFilePathToFirmwareTable(uri)
}
}
}
Here, it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID) returns you the same id for which download was started previously and enqueue returned.
Document for EXTRA_DOWNLOAD_ID indicates that:
Intent extra included with ACTION_DOWNLOAD_COMPLETE intents, indicating the ID (as a long) of the download that just completed.
You have the Uri of file, now simply get the file name to identify the file, you can use following function to get file name
fun getFileName(uri: Uri): String? {
var result: String? = null
when(uri.scheme){
"content" -> {
val cursor: Cursor? = getContentResolver().query(uri, null, null, null, null)
cursor.use {
if (it != null && it.moveToFirst()) {
result = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
}
else -> {
val lastSlashIndex = uri.path?.lastIndexOf('/')
if(lastSlashIndex != null && lastSlashIndex != -1) {
result = uri.path!!.substring(lastSlashIndex + 1)
}
}
}
return result
}
My app uses DownloadManager to download a file , it works perfectly in my real phone.
But downloading is always pending in AVD, it never starts to download.
I'm using Android Studio 4.0.1, and I have created several AVDs, none of them works.
In the AVDs, web browsing works, so the networks should be no problem.
Besides my own app, I have tried several DownloadManager demos from github, none of them works in AVDs.
Help, please.
class MainActivity : AppCompatActivity() {
private val TAG = "MY"
private var enqueue: Long = 0
private lateinit var dm: DownloadManager
inner class MyReceiver: BroadcastReceiver() {
override fun onReceive(context: Context, intent:Intent) {
Log.d(TAG, "Received.")
val action = intent.getAction()
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
val downloadId = intent.getLongExtra(
DownloadManager.EXTRA_DOWNLOAD_ID, 0);
val query = DownloadManager.Query();
query.setFilterById(enqueue);
val c = dm.query(query);
if (c.moveToFirst()) {
val columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
val view = findViewById<ImageView>(R.id.imageView1)
val uriString = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
view.setImageURI(Uri.parse(uriString));
}
}
}
if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)) {
Log.d(TAG,"clicked")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val receiver = MyReceiver()
val f = IntentFilter()
f.addAction( DownloadManager.ACTION_DOWNLOAD_COMPLETE)
f.addAction( ACTION_NOTIFICATION_CLICKED)
registerReceiver(receiver, f)
btDownload.setOnClickListener {
onClick(it)
}
}
fun onClick(view: View) {
dm = getSystemService(DOWNLOAD_SERVICE) as DownloadManager ;
val request = DownloadManager.Request(
Uri.parse("http://www.example.com/abd3234dfdfwefwef.pdf"));
enqueue = dm.enqueue(request);
}
}
I have solved this problem by myself.
In Android Studio (4.0.1 currently), if the AVD images is api v26+, I must set allowed network type to DownloadManager.Request.NETWORK_WIFI.
But these two don't work:
DownloadManager.Request.NETWORK_WIFI
and
DownloadManager.Request.NETWORK_WIFI + DownloadManager.Request.NETWORK_MOBILE