DownloadManager在Android 10 (Q)上无法工作

10

我已经针对这个问题苦苦纠缠了很长时间...... 我正在更新一个应用程序,该应用程序使用DownloadManager执行简单任务,例如将文件下载到外部存储公共目录,即:

Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)

在 Android api 19-28 上一切正常。问题出现在测试 API 29(Q/10)上。Android 实施了作用域存储并弃用了 getExternalStoragePublicDirectory... 因此,我需要找出一个兼容的解决方案来支持 APIs 19-29。我无法使用内部应用程序存储,因为 DownloadManager 会抛出 SecurityException。Android 的文档说明我可以使用 DownloadManager.Request setDestinationUri,甚至提到对于 Android Q 我可以使用 Context.getExternalFilesDir(String)。但是,当我这样做时,路径仍然是模拟路径:

/storage/emulated/0/Android/data/com.my.package.name/files/Download/myFile.xml

下载管理器回调告知下载完成(带有正确的ID),但是我无法从保存下载文件的区域获取下载文件。我检查了文件是否存在,返回为假:

new File("/storage/emulated/0/Android/data/com.my.package.name/files/Download/myFile.xml").exists();

感谢任何帮助。

为了提供上下文,这里添加代码。所以设置下载管理器。

    private void startDownload() {
        IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
        registerReceiver(downloadReceiver, filter);

        String remoteURL= getString(R.string.remote_url);

        DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteUrl));
        request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
        request.setTitle(getString(R.string.download_title));
        request.setDescription(getString(R.string.download_description));
        request.setDestinationUri(Uri.fromFile(new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "myFile.xml")));

        DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
        mainDownloadID= manager.enqueue(request);
    }

检查文件是否存在:

new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "myFile.xml").exists(); //this returns false in the onReceive (and download IDs match)
4个回答

1
尝试将以下内容添加到应用程序标签中的清单文件中:
android:requestLegacyExternalStorage="true"

6
最好采用新的方法,而不是快速修补措施,在下一个Android版本中无法使用。 - Andrew
正如@Andrew所说,我希望得到一个更完整的解决方案来解决这个问题。 - New Guy
是的,它不是很好,但它能够工作,如果你需要快速解决方案,那就是你需要的。 - Сергей Серпивский
@Andrew,请查看此链接:https://commonsware.com/blog/2019/06/07/death-external-storage-end-saga.html - Сергей Серпивский
@Сергей Серпивский 是的,我已经知道这篇博客中的所有信息了,这就是为什么我建议转移到新方法的原因。 - Andrew
我的情况是,我已经添加了android:requestLegacyExternalStorage=true,但我仍然遇到了Android 10上的相同错误。 - Bolt UIX

0

是的,它涉及到作用域存储,但即使您可以使用downloadmanger在Q+中下载文件,也不需要执行android:requestLegacyExternalStorage="true"

我是这样做的。

  1. 清单
  • -->
  • DownloadManager

     val fileName =
         Constants.FILE_NAME + Date().time
    
     val downloadUri = Uri.parse(media.url)
     val request = DownloadManager.Request(
         downloadUri
     )
     request.setAllowedNetworkTypes(
         DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE
     )
         .setAllowedOverRoaming(true).setTitle("一些名称")
         .setDescription("正在下载文件")
         .setDestinationInExternalPublicDir(
             Environment.DIRECTORY_DOWNLOADS, File.separator + FOLDER + File.separator + fileName
         )
    
    
     Toast.makeText(
         context,
         "已成功下载到 ${downloadUri?.path}",
         Toast.LENGTH_LONG
     ).show()
    
     downloadManager.enqueue(request)
    
  • 因此,它将在 Q 以下请求写入权限,但在 Q 和 Q+ 中,它将在 /Download/folder 目录中下载而无需请求权限。

    0

    1
    这很有趣,其他应用程序不需要访问我正在下载的文件。问题是,如何持久化用户第一次选择的文件位置?我不想每次都询问用户,如果我已经下载了它,我只想从用户最初告诉我的存储位置中提取它。 - New Guy
    最后一个链接展示了如何获取在设备重启后仍然保持的权限。如果其他应用程序不需要访问你下载的文件,那么最好将它们存储在你的应用程序的私有目录中,这样会更简单,因为它不需要权限。 - Andrew
    1
    正确,但我想使用DownloadManager来下载我需要的文件,但它无法接受应用程序私有目录的位置作为目标(将抛出SecurityException)。 - New Guy

    -1

    使用此代码并享受,此代码使用RxJava进行网络调用:

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

    在调用方,你必须编写如下代码:

    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
            }, { 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 to Downloads Folder", Toast.LENGTH_SHORT).show()
                }
            })
    }
    

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接