57

Lets say I'm developing a chat app that is able to share with others ANY kind of files (no mimetype restriction): like images, videos, documents, but also compressed files like zip, rar, apk or even less frequent types of files like photoshop or autocad files, for example.

In Android 9 or lower I directly download those files to Download directory, but that's now impossible in Android 10 without showing an Intent to the user to ask where to download them...

Impossible? but then why Google Chrome or other browsers are able to do that? They in fact still download files to Download directory without asking user in Android 10.

I first analyzed Whatsapp to see how they achieve it but they make use of requestLegacyExternalStorage attribute on AndroidManifest. But then I analyzed Chrome and it targets Android 10 without using requestLegacyExternalStorage. How is that possible?

I have been googling for some days already how apps can download a file directly to Download directory on Android 10 (Q) without having to ask user where to place it, the same way Chrome does.

I have read android for developers documentation, lots of questions on Stackoverflow, blog posts over the Internet and Google Groups but still I haven't found a way to keep doing exactly the same as in Android 9 nor even a solution that plenty satisfies me.

What I've tried so far:

  • Open SAF with an ACTION_CREATE_DOCUMENT Intent to ask for permission but apparently there's no way to open it silently. An Activity is always opened to ask user where to place the file. But am I supposed to open this Intent on every file? My app can download chat files automatically being on background. Not a feasible solution.

  • Get grant access using SAF at the beginning of the app with an uri pointing to any directory for download contents:

        StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
        i = sm.getPrimaryStorageVolume().createOpenDocumentTreeIntent();
    

    What an ugly activity to ask user for permission, isn't it? Even though this is NOT what Google Chrome does.

  • Or again by using ACTION_CREATE_DOCUMENT, save the Uri that I get in onActivityResult() and use grantPermission() and getContentResolver().takePersistableUriPermission(). But this does not create a directory but a file.

  • I've also tried to get MediaStore.Downloads.INTERNAL_CONTENT_URI or MediaStore.Downloads.EXTERNAL_CONTENT_URI and save a file by using Context.getContentResolver.insert(), but what a coincidence although they are annotated as @NonNull they in fact return... NULL

  • Adding requestLegacyExternalStorage="false" as an attribute of Application label of my AndroidManifest.xml. But this is just a patch for developers in order to gain time until they make changes and adapt their code. Besides still this is not what Google Chrome does.

  • getFilesDir() and getExternalFilesDir() and getExternalFilesDirs() are still available but files stored on those directories are deleted when my app is uninstalled. Users expect to keep their files when uninstalling my app. Again not a feasible solution for me.

My temporary solution:

I've found a workaround that makes it possible to download wherever you want without adding requestLegacyExternalStorage="false".

It consists on obtaining an Uri from a File object by using:

val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(downloadDir, fileName)
val authority = "${context.packageName}.provider"
val accessibleUri = FileProvider.getUriForFile(context, authority, file)

Having a provider_paths.xml

<paths>
    <external-path name="external_files" path="."/>
</paths>

And setting it on AndroidManifest.xml:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />
</provider>

The problem:

It make use of getExternalStoragePublicDirectory method which is deprecated as of Android Q and extremmely likely will be removed on Android 11. You could think that you can make your own path manually as you know the real path (/storage/emulated/0/Download/) and keep creating a File object, but what if Google decices to change Download directory path on Android 11?

I'm afraid this is not a long term solution, so

My question:

How can I achieve this without using a deprecated method? And a bonus question How the hell Google Chrome accomplish getting access to Download directory?

Rubén Viguera
  • 3,277
  • 1
  • 17
  • 31
  • There are some alternatives written in the deprecated note here: https://developer.android.com/reference/android/os/Environment#getExternalStoragePublicDirectory(java.lang.String) – Saurabh Thorat Nov 29 '19 at 10:38
  • 1
    `but that's now impossible in Android 10 without showing an Intent to the user to ask where to download them...` No. Not at all. You can still use getFilesDir() and getExternalFilesDir() and getExternalFilesDirs(). – blackapps Nov 29 '19 at 10:38
  • In your list i miss ACTION_OPEN_DOCUMENT_TREE. – blackapps Nov 29 '19 at 10:42
  • `how apps can download a file directly to Download directory on Android 10 (Q) without having to ask user where to place it,`. Untested: Doesn't the DownloadManager download by default to that folder? – blackapps Nov 29 '19 at 10:43
  • @aaurabh-thorat Ah, I forgot commenting that, I download onto Download directory in order to have permanent files. Users expect to keep their files when they uninstall my app, so getFilesDir() and getExternalFilesDir() and getExternalFilesDirs() are not valid solutions for me – Rubén Viguera Nov 29 '19 at 10:44
  • You can download to getExternalFIlesDir(). Then save file to MediaStore and then delete the first. – blackapps Nov 29 '19 at 10:50
  • @blackapps "In your list i miss ACTION_OPEN_DOCUMENT_TREE" Is the second point, but instead I use createOpenDocumentTreeIntent() method which internally uses that action – Rubén Viguera Nov 29 '19 at 10:50
  • No. That is quite something different. Use ACTION_OPEN_DOCUMENT_TREE and you will see. – blackapps Nov 29 '19 at 10:51
  • @blackapps "You can download to getExternalFIlesDir(). Then save file to MediaStore and then delete the first." but as I said MediaStore.Downloads.INTERNAL_CONTENT_URI returns null. Maybe I have to combine some of the other solutions with this to have a notnull Uri but I haven't found it yet. – Rubén Viguera Nov 29 '19 at 10:53
  • If you have problems with saving to MediaStore then post your code in a new thread and we can have a look. It just works on Android Q too. Ok. Sorry. That will not go to the Download folder then. So useless. Sorry. It will go to DCIM or Pictures. But who cares?? – blackapps Nov 29 '19 at 10:54
  • @blackapps "It will go to DCIM or Pictures. But who cares??" I could separate images, videos, audios or whatever to their actual directory but still there are some other kind of files that does not belong to that categories such as compressed files like ZIP, RAR or APKs or propietary files like photoshop, autocad, guitar pro tabs or whatever even some types of images, videos, etc that are not valid on those directories becouse they doesn't fit their mimetype filttering ("image/*" or "video/*" for example) – Rubén Viguera Nov 29 '19 at 11:05
  • @blackapps "No. That is quite something different. Use ACTION_OPEN_DOCUMENT_TREE and you will see" Ok, I'll take a look – Rubén Viguera Nov 29 '19 at 11:06
  • `but still there are some other kind of files that does not belong to that categories ` Yes. But who cares? The MediaStore or the Gallery apps just will not show them. It's time you experiment a bit. – blackapps Nov 29 '19 at 11:08
  • Use ACTION_OPEN_DOCUMENT_TREE to let the user choose the primary partition or the micro SD card. Then create your own app folder. In the app folder you can make as much folders as you want to organise your files. – blackapps Nov 29 '19 at 11:17
  • If you only in the MediaStore (to DCIM or Pictures) then you have no access to them anymore unless you also used once ACTION_OPEN_DOCUMENT_TREE. So it was a bad advise. Sorry. Use ACTION_OPEN_DOCUMENT_TREE like above only. Did you try DownloadManager? – blackapps Nov 29 '19 at 11:28
  • 2
    "get MediaStore.Downloads.INTERNAL_CONTENT_URI or MediaStore.Downloads.EXTERNAL_CONTENT_URI and save a file by using Context.getContentResolver.insert()" is supposed to be the solution. You might consider asking a separate Stack Overflow question with a [mcve] demonstrating how you tried this. – CommonsWare Nov 29 '19 at 12:06
  • @CommonsWare. Thank for the hint. Tried both. The internal brings an UnsuppotedOperationException. The external works but i did not yet see a posibility to create a subdirectory. But the problem stays: Once all your file are in Download you will still need an ACTION_OPEN_DOCUMENT_TREE to handle them. – blackapps Nov 29 '19 at 14:44
  • @CommonsWare, I have tried again to use MediaStore.Downloads.EXTERNAL_CONTENT_URI and surprisingly now it returns a notNull Uri. I've tried insert method and it successfully creates a file but it is not accessible by Google Files. I think this happens because the file belongs to my app and Google Files does not ask for READ_EXTERNAL_STORAGE permission. – Rubén Viguera Nov 29 '19 at 15:39
  • @blackapps you can create a subdirectory as CommonsWare answers in this other question: https://stackoverflow.com/questions/56468539/getexternalstoragepublicdirectory-deprecated-in-android-q – Rubén Viguera Nov 29 '19 at 15:40
  • Yes, i already used that. It works for DCIM and Pictures directory but not for Download as i told you before. – blackapps Nov 30 '19 at 08:31
  • Eh... well it is possible to make subdirs in Download. .. Just have to know how ;-). – blackapps Nov 30 '19 at 08:51

5 Answers5

43

More than 10 months have passed and yet not a satisfying answer for me have been made. So I'll answer my own question.

As @CommonsWare states in a comment, "get MediaStore.Downloads.INTERNAL_CONTENT_URI or MediaStore.Downloads.EXTERNAL_CONTENT_URI and save a file by using Context.getContentResolver.insert()" is supposed to be the solution. I double checked and found out this is true and I was wrong saying it doesn't work. But...

I found it tricky to use ContentResolver and I was unable to make it work properly. I'll make a separate question with it but I kept investigating and found a somehow satisfying solution.

MY SOLUTION:

Basically you have to download to any directory owned by your app and then copy to Downloads folder.

  1. Configure your app:

    • Add provider_paths.xml to xml resource folder

      <paths xmlns:android="http://schemas.android.com/apk/res/android">
          <external-path name="external_files" path="."/>
      </paths>
      
    • In your manifest add a FileProvider:

      <application>
          <provider
               android:name="androidx.core.content.FileProvider"
               android:authorities="${applicationId}.provider"
               android:exported="false"
               android:grantUriPermissions="true">
               <meta-data
                   android:name="android.support.FILE_PROVIDER_PATHS"
                   android:resource="@xml/provider_paths" />
           </provider>
       </application>
      
  2. Prepare to download files to any directory your app owns, such as getFilesDir(), getExternalFilesDir(), getCacheDir() or getExternalCacheDir().

    val privateDir = context.getFilesDir()
    
  3. Download file taking its progress into account (DIY):

    val downloadedFile = myFancyMethodToDownloadToAnyDir(url, privateDir, fileName)
    
  4. Once downloaded you can make any threatment to the file if you'd like to.

  5. Copy it to Downloads folder:

    //This will be used only on android P-
    private val DOWNLOAD_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
    
    val finalUri : Uri? = copyFileToDownloads(context, downloadedFile)
    
    fun copyFileToDownloads(context: Context, downloadedFile: File): Uri? {
        val resolver = context.contentResolver
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val contentValues = ContentValues().apply {
                put(MediaStore.MediaColumns.DISPLAY_NAME, getName(downloadedFile))
                put(MediaStore.MediaColumns.MIME_TYPE, getMimeType(downloadedFile))
                put(MediaStore.MediaColumns.SIZE, getFileSize(downloadedFile))
            }
            resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
        } else {
            val authority = "${context.packageName}.provider"
            val destinyFile = File(DOWNLOAD_DIR, getName(downloadedFile))
            FileProvider.getUriForFile(context, authority, destinyFile)
        }?.also { downloadedUri ->
            resolver.openOutputStream(downloadedUri).use { outputStream ->
                val brr = ByteArray(1024)
                var len: Int
                val bufferedInputStream = BufferedInputStream(FileInputStream(downloadedFile.absoluteFile))
                while ((bufferedInputStream.read(brr, 0, brr.size).also { len = it }) != -1) {
                    outputStream?.write(brr, 0, len)
                }
                outputStream?.flush()
                bufferedInputStream.close()
            }
        }
    }
    
  6. Once in download folder you can open file from app like this:

    val authority = "${context.packageName}.provider"
    val intent = Intent(Intent.ACTION_VIEW).apply {
        setDataAndType(finalUri, getMimeTypeForUri(finalUri))
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP) {
            addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
        } else {
            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        }
    }
    try {
        context.startActivity(Intent.createChooser(intent, chooseAppToOpenWith))
    } catch (e: Exception) {
        Toast.makeText(context, "Error opening file", Toast.LENGTH_LONG).show()
    }
    
    //Kitkat or above
    fun getMimeTypeForUri(context: Context, finalUri: Uri) : String =
        DocumentFile.fromSingleUri(context, finalUri)?.type ?: "application/octet-stream"
    
    //Just in case this is for Android 4.3 or below
    fun getMimeTypeForFile(finalFile: File) : String =
        DocumentFile.fromFile(it)?.type ?: "application/octet-stream"
    

Pros:

  • Downloaded files survives to app uninstallation

  • Also allows you to know its progress while downloading

  • You still can open them from your app once moved, as the file still belongs to your app.

  • write_external_storage permission is not required for Android Q+, just for this purpose:

    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />
    

Cons:

  • You won't have access to downloaded files once after clearing your app data or uninstalling and reinstalling again (they no longer belongs to your app unless you ask for permission)
  • Device must have more free space to be able to copy every file from its original directory to its final destination. This is important speacially for large files. Although if you have access to the original inputStream you could directly write to downloadedUri instead of copying from an intermediary file.

If this approach is enough for you then give it a try.

Rubén Viguera
  • 3,277
  • 1
  • 17
  • 31
  • I think your solution for copying file to download folder is good but it needs `put(MediaStore.MediaColumns.IS_PENDING, 1)` for `ContentResolver` to successfully insert `contentValues` – Javad Vovin Dec 22 '20 at 13:37
  • 1
    I'm afraid it is not necessary. I am using this aproach in production and have been working for almost a year now. IS_PENDING could be needed if you do not copy from an owned directory, but just directly write into Downloads directory. I would love to see an answer using IS_PENDING as I was unable to make it work. – Rubén Viguera Dec 22 '20 at 16:51
  • 1
    In my case it was needed, I download some file into app data folder and copy it into download folder, without IS_PENDING the result `Uri` is `null`, but with it the result `Uri` is valid. – Javad Vovin Dec 26 '20 at 07:01
  • 1
    Just like you I spent hours to find out how to write a file to the download folder without explicitly having to ask the user for the location, just like other google apps do it. Its a shame that google makes this so complicated to support all android versions. Anyways, your solution works perfectly for me, thanks for sharing it. You saved me a lot of time. – ice_chrysler Dec 13 '21 at 15:30
  • Great answer and completely solves my problem – Skullper Aug 09 '22 at 12:53
12

You can use the Android DownloadManager.Request. It will need the WRITE_EXTERNAL_STORAGE Persmission until Android 9. From Android 10/ Q and above it will not need any permission (it seems it handle the permission itself).

If you want to open the file afterwards, you will need the user's permission instead (also if you only want to open it within an external app (e.g. PDF-Reader).

You can use the download manager like this:

DownloadManager.Request request = new DownloadManager.Request(<download uri>);
request.addRequestHeader("Accept", "application/pdf");
    
// Save the file in the "Downloads" folder of SDCARD
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,filename);

DownloadManager downloadManager = (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
downloadManager.enqueue(request);

This are the references: https://developer.android.com/reference/kotlin/android/app/DownloadManager.Request?hl=en#setDestinationInExternalPublicDir(kotlin.String,%20kotlin.String)

https://developer.android.com/training/data-storage

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
beal
  • 435
  • 4
  • 15
  • This could be a solution for anyone else, but not in my case, as it doesn't allows me to know its progress while downloading. I'd like to show a progressBar with its progress specially for large files. – Rubén Viguera Oct 14 '20 at 15:46
  • You can get the progress as described here: https://stackoverflow.com/questions/7824835/android-downloadmanager-progress – Hyndrix Nov 12 '20 at 15:23
  • Not for local file!! java.lang.IllegalArgumentException: Can only download HTTP/HTTPS URIs: file:///storage/emulated/0/Android/... – Fortran Dec 17 '20 at 13:30
  • 1
    I can only speak for my projects, but as of early 2021 Android's Download Manager is having issues on some devices I am seeing a spike in complaints (And I have verified it on my device) about it either never downloading files or taking a full day to actually start the download. Just some info for anyone wanting to go this route. – PGMacDesign Apr 23 '21 at 04:05
4

I thought that it would best be to write the answer in java, as a lot of legacy code exists out there which would have to be updated. Also I have no idea what kind of file you want to store and so, here, I took an example of a bitmap. If you want to save any other kind of file you just have to create an OutputStream of it, and for the remaining part of it you can follow the code I write below. Now, here, this function is only going to handle the saving for Android 10+ and hence the annotation. I hope you have the prerequisite knowledge on how to save files in lower android versions

@RequiresApi(api = Build.VERSION_CODES.Q)
public void saveBitmapToDownloads(Bitmap bitmap) {
    ContentValues contentValues = new ContentValues();

    // Enter the name of the file here. Note the extension isn't necessary
    contentValues.put(MediaStore.Downloads.DISPLAY_NAME, "Test.jpg");
    // Here we define the file type. Do check the MIME_TYPE for your file. For jpegs it is "image/jpeg"
    contentValues.put(MediaStore.Downloads.MIME_TYPE, "image/jpeg");
    contentValues.put(MediaStore.Downloads.IS_PENDING, true);

    Uri uri = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
    Uri itemUri = getContentResolver().insert(uri, contentValues);

    if (itemUri != null) {
        try {
            OutputStream outputStream = getContentResolver().openOutputStream(itemUri);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
            outputStream.close();
            contentValues.put(MediaStore.Images.Media.IS_PENDING, false);
            getContentResolver().update(itemUri, contentValues, null, null);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Now if you want to create a sub directory i.e. a folder inside the Downloads folder you could add this to contentValues

contentValues.put(MediaStore.Downloads.RELATIVE_PATH, "Download/" + "Folder Name");

This will create a folder named "Folder Name" and store "Test.jpg" inside that folder.

Also do note this doesn't require any permissions in the manifest which implies it doesn't need runtime permissions as well. If you want the Kotlin version do ask me in the comments. I would happily and readily provide the Kotlin method for the same.

Joel
  • 353
  • 2
  • 7
  • hey,because of my requirement what if i want to specify extension?? – Vivek Thummar Jun 23 '21 at 11:46
  • Yes you can add the extension for your file in DISPLAY_NAME and can check for your file's MIME_TYPE from here. https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types – Joel Jun 24 '21 at 14:21
1

I'm afraid your solution is not for "Directly downloading to Downloads directory", to achieve that you can use this code and enjoy! This code uses RxJava for network call, you can use whatever is comfortable for you, this will work for all Android versions:

import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import io.reactivex.Observable
import io.reactivex.ObservableEmitter
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.ResponseBody
import java.io.*
import java.net.HttpURLConnection
import java.util.concurrent.TimeUnit
class FileDownloader(
         private val context: Context,
         private val url: String,
         private val fileName: String
         ) {
    
        private val okHttpClient: OkHttpClient = OkHttpClient.Builder()
            .connectTimeout(60, TimeUnit.SECONDS)
            .readTimeout(60, TimeUnit.SECONDS)
            .build()
    
        private val errorMessage = "File couldn't be downloaded"
        private val bufferLengthBytes: Int = 1024 * 4
    
        fun download(): Observable<Int> {
            return Observable.create<Int> { emitter ->
    
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // To Download File for Android 10 and above
                    val content = ContentValues().apply {
                        put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
                        put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
                    }
                    val uri = context.contentResolver.insert(
                        MediaStore.Downloads.EXTERNAL_CONTENT_URI,
                        content
                    )
                    uri?.apply {
                        val responseBody = getResponseBody(url)
                        if (responseBody != null
                        ) {
                            responseBody.byteStream().use { inputStream ->
                                context.contentResolver.openOutputStream(uri)?.use { fileOutStream ->
                                    writeOutStream(
                                        inStream = inputStream,
                                        outStream = fileOutStream,
                                        contentLength = responseBody.contentLength(),
                                        emitter = emitter
                                    )
                                }
                                emitter.onComplete()
                            }
                        } else {
                            emitter.onError(Throwable(errorMessage))
                        }
                    }
                }
                    else { // For Android versions below than 10
                        val directory = File(
                            Environment.getExternalStoragePublicDirectory(
                                Environment.DIRECTORY_DOWNLOADS).absolutePath
                        ).apply {
                            if (!exists()) {
                                mkdir()
                            }
                        }
    
                    val file = File(directory, fileName)
                    val responseBody = getResponseBody(url)
    
                    if (responseBody != null) {
                        responseBody.byteStream().use { inputStream ->
                            file.outputStream().use { fileOutStream ->
                                writeOutStream(
                                    inStream = inputStream,
                                    outStream = fileOutStream,
                                    contentLength = responseBody.contentLength(),
                                    emitter = emitter
                                )
                            }
                            emitter.onComplete()
                        }
    
                    } else {
                        emitter.onError(Throwable(errorMessage))
                    }
                }
            }
        }
    
        private fun getResponseBody(url: String): ResponseBody? {
            val response = okHttpClient.newCall(Request.Builder().url(url).build()).execute()
    
            return if (response.code >= HttpURLConnection.HTTP_OK &&
                response.code < HttpURLConnection.HTTP_MULT_CHOICE &&
                response.body != null
            )
                response.body
            else
                null
        }
    
        private fun writeOutStream(
            inStream: InputStream,
            outStream: OutputStream,
            contentLength: Long,
            emitter: ObservableEmitter<Int>) {
                var bytesCopied = 0
                val buffer = ByteArray(bufferLengthBytes)
                var bytes = inStream.read(buffer)
                while (bytes >= 0) {
                    outStream.write(buffer, 0, bytes)
                    bytesCopied += bytes
                    bytes = inStream.read(buffer)
    //                emitter.onNext(
                        ((bytesCopied * 100) / contentLength).toInt()
    //                )
                }
        outStream.flush()
        outStream.close()
        inStream.close()
        }
    }

On the calling side you can do this:

private fun downloadFileFromUrl(context: Context, url: String, fileName: String) {
    FileDownloader(
        context = context,
        url = url,
        fileName = fileName
    ).download()
        .throttleFirst(2, TimeUnit.SECONDS)
        .toFlowable(BackpressureStrategy.LATEST)
        .subscribeOn(Schedulers.io())
        .observeOn(mainThread())
        .subscribe({
            // onNext: Downloading in progress
            Timber.i( "File downloaded $it%")
        }, { error ->
            // onError: Download Error
            requireContext()?.apply {
                Toast.makeText(this, error.message, Toast.LENGTH_SHORT).show()
            }
        }, {
            // onComplete: Download Complete
            requireContext()?.apply {
                Toast.makeText(this, "File downloaded!", Toast.LENGTH_SHORT).show()
            }
        })
}
Mohsin H
  • 11
  • 3
  • how can we get the path and use it to show the file? – jlively Nov 15 '21 at 17:54
  • @jlively val directory = File( Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS).absolutePath ) I used path of Downloads folder – Mohsin H Nov 16 '21 at 21:32
  • hey, thanks! That's for Android 9 and below, how about 10 and above? – jlively Nov 17 '21 at 15:50
  • @jlively for Android 10 and you can get path by this code: val content = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) } val uri = context.contentResolver.insert( MediaStore.Downloads.EXTERNAL_CONTENT_URI, content ) val directory = File(uri.path) – Mohsin H Dec 07 '21 at 20:35
0

When you want to save documents from ResponseBody in downloads folder use this

private fun saveFileToStorage(
    body: ResponseBody,
    filename: String,
    consignmentId: String,
    context: Context
) {

    val contentResolver = context.contentResolver
    var inputStream: InputStream? = null
    var outputStream: OutputStream? = null
    var fileZizeDownloaded: Long = 0


    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        val values = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
            put(MediaStore.MediaColumns.MIME_TYPE, "application/pdf")
            put(
                MediaStore.MediaColumns.RELATIVE_PATH, 
                Environment.DIRECTORY_DOWNLOADS
            )
        }

        val uri = 
           contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, 
            values)
        try {
            var pfd: ParcelFileDescriptor?=null
            try {
                pfd = 
                uri?.let { contentResolver.openFileDescriptor(it, "w")}!!
                val buf = ByteArray(4 * 1024)
                inputStream = body.byteStream()
                outputStream = FileOutputStream(pfd.fileDescriptor)
                var len: Int

                while (true) {
                    val read = inputStream.read(buf)
                    if (read == -1) {
                        break
                    }
                    outputStream.write(buf, 0, read)
                    fileZizeDownloaded += read.toLong()
                }

                outputStream.flush()
              } catch (e: java.lang.Exception) {
                e.printStackTrace()
            } finally {
                inputStream?.close()
                outputStream?.close()
                pfd?.close()
            }
            values.clear()
            values.put(MediaStore.Video.Media.IS_PENDING, 0)
            if (uri != null) {
                context.contentResolver.update(uri, values, null, null)
                outputStream = context.contentResolver.openOutputStream(uri)
            }
            if (outputStream == null) {
                throw IOException("Failed to get output stream.")
            }
            
        } catch (e: IOException) {
            if (uri != null) {
                context.contentResolver.delete(uri, null, null)  
            } 
        } finally {
            outputStream?.close()
        }
    } else {
        val directory_path =
                 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
        val file = File(directory_path)
        if (!file.exists()) {
            file.mkdirs()
        }
        val targetPdf = directory_path + filename
        val filePath = File(targetPdf)

        try {
            val fileReader = ByteArray(4 *1024)
            inputStream = body.byteStream()
            outputStream = FileOutputStream(filePath)
            while (true) {
                val read = inputStream.read(fileReader)
                if (read == -1) {
                    break
                }
                outputStream.write(fileReader, 0, read)
                fileZizeDownloaded += read.toLong()

            }
            outputStream.flush()
            
        } catch (e: IOException) {
            e.printStackTrace()
           
        } finally {
            inputStream?.close()
            outputStream?.close()
        }
    }
}
Nadeem Iqbal
  • 2,357
  • 1
  • 28
  • 43