Identifying the particular file after downloaded using Download Manager in Android - android

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
}

Related

DownloadManager returned Uri is unusable in Android 13

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")
}
)
}
}
}
}

How to get path /storage/emulated/0/Download/file_name.mime_type for Android 10 and above

I am saving a file inside the Downloads directory of the device (Android 11) to be viewed later by my app. I'm allowing multiple file types like pdf, word etc. I was able to save the file like this: (I got this code sample from here)
#TargetApi(29)
private suspend fun downloadQ(
url: String,
filename: String,
mimeType: String
) =
withContext(Dispatchers.IO) {
val response = ok.newCall(Request.Builder().url(url).build()).execute()
if (response.isSuccessful) {
val values = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, filename)
put(MediaStore.Downloads.MIME_TYPE, mimeType)
put(MediaStore.Downloads.IS_PENDING, 1)
}
val resolver = context.contentResolver
val uri =
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
uri?.let {
resolver.openOutputStream(uri)?.use { outputStream ->
val sink = outputStream.sink().buffer()
response.body()?.source()?.let { sink.writeAll(it) }
sink.close()
}
values.clear()
values.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(uri, values, null, null)
} ?: throw RuntimeException("MediaStore failed for some reason")
} else {
throw RuntimeException("OkHttp failed for some reason")
}
}
But when I tried to retrieve the file, I tried with the following ways that did not work:
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID)
val id = cursor.getLong(idColumn)
Log.d("uri id ","$id")
val contentUri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI,id)
This approach threw an exception:
java.lang.IllegalArgumentException: Failed to find configured root that contains /external/downloads/78
I got this ID (here 78) from the query and cursor from ContentResolver.query() and I hoped it to return the Uri from which I could get the File.
The second approach was this:
val uri = MediaStore.Downloads.getContentUri("external",id)
uri.path?.let { filePath ->
Log.d("uri path ",filePath)
val file = File(filePath)
} ?: Log.d("uri path ","null")
I used external as the directory based on this answer, but this approach also threw the same exception
java.lang.IllegalArgumentException: Failed to find configured root that contains /external/downloads/78
At the end what ended up working was hardcoding something like this after I used a file explorer app to view the exact directory path:
val file = File("storage/emulated/0/Download/$name.$extension")
So my question is, how do I get the value of this path dynamically, and is this path the same for all devices that can be used like this way?
EDIT: I also wanted to know if I am using the filename and it's extension to view the file, then if user downloads another file with same name then how do I make sure that correct file is opened? (even if i make a separate directory for my app inside Download, user could still download the same file twice that has a name like storage/emulated/0/Download/myDir/file(2).extension )
Try with the following code it will help you.
private fun readFile(){
val FILENAME = "user_details.txt"
val dir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.toString() + "/" + "folderName"
)
} else {
File(
Environment.getExternalStorageDirectory()
.toString() + "/${Environment.DIRECTORY_DOWNLOADS}/" + "folderName"
)
}
dir.apply {
if(this.exists()) File(this, FILENAME).apply {
FileInputStream(this).apply {
val stringBuffer = StringBuffer()
var i: Int
while (this.read().also { i = it } != -1) {
stringBuffer.append(i.toChar())
}
close()
}
}
}
}
You can use
{Environment.DIRECTORY_DOWNLOADS} + "/folderName/file_name.mime_type"
/storage/emulated/0/Download/12d82c65-00a5-4c0a-85bc-238c28005c33.bin

Deleting physical file using Android content provider MediaStore.Files.getContentUri(VOLUME_NAME_EXTERNAL)

My current Android Application stores pdf files on external storage using
val contentUri = MediaStore.Files.getContentUri(VOLUME_NAME_EXTERNAL)
The application creates a sub folder in the standard Documents folder.
My manifest contains
android:requestLegacyExternalStorage = true
For Android 30 I request the following
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
I have a background worker that attempts to clear all down loaded files, the code I employ is as follows:-
#RequiresApi(Build.VERSION_CODES.Q)
override suspend fun doActualFlowedWork(): Result {
if (hasSdkHigherThan(Build.VERSION_CODES.P)) {
clearDownloadFiles()
} else {
clearDownloadLegacyFiles()
}
return result ?: Result.success()
}
#Suppress("BlockingMethodInNonBlockingContext")
#RequiresApi(Build.VERSION_CODES.Q)
private fun clearDownloadFiles() {
val resolver = context.contentResolver
val relativeLocation = "${Environment.DIRECTORY_DOCUMENTS}${MY_SUB_FOLDER}"
val contentUri = MediaStore.Files.getContentUri(VOLUME_NAME_EXTERNAL)
resolver.query(
contentUri,
arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.RELATIVE_PATH),
"${MediaStore.MediaColumns.RELATIVE_PATH}=?",
arrayOf(relativeLocation),
null
).use { cursor ->
cursor?.let {
while (it.moveToNext()) {
val idIndex = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
val displayNameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME)
val relativePathNameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.RELATIVE_PATH)
if (cursor.getString(relativePathNameIndex) == relativeLocation) {
val fileContentUri = MediaStore.Files.getContentUri(VOLUME_NAME_EXTERNAL, cursor.getLong(idIndex))
val count = context.contentResolver.delete(fileContentUri, null, null)
if (count == 0) Timber.e("FAILED to clear downloaded file = ${cursor.getString(displayNameIndex)}")
else Timber.i("Cleared downloaded file = ${cursor.getString(displayNameIndex)}")
}
}
}
}
}
#Suppress("DEPRECATION")
private fun clearDownloadLegacyFiles() {
val documentsFolder = File(Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS)
if (!documentsFolder.exists()) return
val mendeleyLiteSubFolder = File(Environment.getExternalStorageDirectory(), "${Environment.DIRECTORY_DOCUMENTS}$MY_SUB_FOLDER")
if (!mendeleyLiteSubFolder.exists()) return
val downloadFiles = mendeleyLiteSubFolder.listFiles()
downloadFiles?.forEach { downloadFile ->
if (downloadFile.exists()) downloadFile.delete()
Timber.i("Clearing downloaded file = $downloadFile")
}
}
This clear down worker completes OK, with the logs showing the files have been deleted
however when I use Android Studio Device File Explorer to view my Document sub folder the physical pdf files are still present.
Are my expectations incorrect?
What does this code achieve context.contentResolver.delete(fileContentUri, null, null)?
How do I delete physical files from my sub folder?

How to hande different results of DownloadManager

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; }
}

Android Database download online

Hi I am new in Android development and my app is completely database oriented.
In my app I am using the method of copying a database file from the assets folder.
It will increase the size of the apk.
I want to copy it from the internet the first time my app runs on the phone.
How do I download the database file to my app database folder.
This is how I did it:
I have an implementation of DownloadManager that deals with downloading the DB. In my case the DB is significantly big so DownloadManager is a good option for effectively dealing with large downloads.
One thing to note when implementing DownloadManager; It's recommended that you download files first as temporary files and then move them to the final location. This is to avoid weird security issues I faced when doing so. Also download notification visibility may affect what permissions are required, if you decide to have no notifications at all you need to add a DOWNLOAD_WITHOUT_NOTIFICATION.
Permissions required:
<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)
}
So in case you want to notify a running Activity/Fragment, you only need to pass a DownloadCompleteCallback implementation to this component to get it.
In case you are using Room, just make sure your implementation of RoomDatabase uses the following on your getInstance method
.createFromFile( dbFileHelper.getPermanentDbFile() )
just copy it from web instead of copying it form assets. Hint

Categories

Resources