如何直接将文件下载到Android Q(Android 10)的下载目录中 [英] How to directly download a file to Download directory on Android Q (Android 10)

查看:296
本文介绍了如何直接将文件下载到Android Q(Android 10)的下载目录中的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

可以说我正在开发一个聊天应用程序,它可以与他人共享任何类型的文件(没有模仿类型的限制):例如图像,视频,文档,还可以压缩文件,例如zip,rar,apk或不那么频繁例如,photoshop或autocad文件等文件类型.

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.

在Android 9或更低版本中,我直接将这些文件下载到下载"目录中,但是在Android 10中,如果不向用户显示向其询问将它们下载到何处的意图,这是不可能的...

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...

不可能吗?但是,为什么谷歌浏览器或其他浏览器能够做到这一点?实际上,他们仍然在不询问Android 10用户的情况下将文件下载到下载"目录.

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.

我首先分析了Whatsapp,以了解它们是如何实现的,但是他们利用了AndroidManifest上的requestLegacyExternalStorage属性.但是后来我分析了Chrome,它以不使用requestLegacyExternalStorage的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?

我已经在Google上搜索了几天,应用程序如何直接将文件下载到Android 10(Q)上的下载目录,而无需询问用户将文件放置在何处,就像Chrome浏览器一样.

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.

我已经阅读了android的开发人员文档,关于Stackoverflow的许多问题,互联网和Google网上论坛上的博客文章,但是我仍然找不到一种与Android 9完全相同的方法,甚至找不到足够的解决方案令我满意.

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.

到目前为止,我已经尝试过:

  • 使用ACTION_CREATE_DOCUMENT来打开SAF以寻求许可,但是显然无法静默打开它.始终会打开一个活动,以询问用户文件的放置位置.但是我应该在每个文件上打开此Intent吗?我的应用程序可以自动在后台下载聊天文件.这不是可行的解决方案.

  • 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.

在应用程序的开头使用SAF获得授权访问权限,并且uri指向任何可下载内容的目录:

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

向用户寻求许可是多么丑陋的活动,不是吗?即使这不是Google Chrome的功能.

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

或者再次使用ACTION_CREATE_DOCUMENT,保存在onActivityResult()中获得的Uri,并使用grantPermission()和getContentResolver().takePersistableUriPermission().但这不会创建目录,而是创建文件.

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.

我还尝试获取MediaStore.Downloads.INTERNAL_CONTENT_URI或MediaStore.Downloads.EXTERNAL_CONTENT_URI并使用Context.getContentResolver.insert()保存文件,但这是一个巧合,尽管它们被标注为@NonNull实际上返回... NULL

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

添加requestLegacyExternalStorage ="false"作为我的AndroidManifest.xml的应用程序标签的属性.但这只是开发人员的补丁程序,目的是在开发人员进行更改和修改其代码之前获得时间.除此之外,这不是Google Chrome的功能.

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()和getExternalFilesDir()以及getExternalFilesDirs()仍然可用,但是在卸载我的应用程序时,存储在这些目录中的文件将被删除.用户希望在卸载我的应用程序时保留其文件.再次对我来说不是可行的解决方案.

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.

我的临时解决方案:

我找到了一种解决方法,可以在不添加requestLegacyExternalStorage ="false"的情况下,在任何需要的位置进行下载.

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

它包括使用以下命令从File对象获取Uri:

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)

具有provider_paths.xml

Having a provider_paths.xml

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

并在AndroidManifest.xml上进行设置:

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>

问题:

它利用getExternalStoragePublicDirectory方法,该方法从Android Q开始不推荐使用,并且极有可能在Android 11上被删除.您可能认为您可以手动创建自己的路径,因为您知道真实路径(/storage/emulated/0 /Download/)并继续创建File对象,但是如果Google决定更改Android 11上的Download目录路径,该怎么办?

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

我的问题:

如何在不使用不推荐使用的方法的情况下实现此目标?还有一个额外的问题:Google Chrome浏览器如何完成对下载目录的访问?

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?

推荐答案

已经过去了10多个月,但对我来说却没有令人满意的答案.所以我会回答我自己的问题.

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

如@CommonsWare在评论中所述,获取MediaStore.Downloads.INTERNAL_CONTENT_URI或MediaStore.Downloads.EXTERNAL_CONTENT_URI并使用Context.getContentResolver.insert()保存文件". 应该是解决方案.我仔细检查了一下,发现这是真的,但我说错了,这是错误的.但是...

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...

我发现使用ContentResolver非常棘手,但无法使其正常运行.我将单独提出一个问题,但我一直在调查,找到了某种令人满意的解决方案.

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.

我的解决方案:

基本上,您必须下载到应用程序拥有的任何目录,然后复制到下载"文件夹.

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

  1. 配置您的应用:

  1. Configure your app:

  • provider_paths.xml 添加到 xml 资源文件夹

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="external_files" path="."/>
</paths>

  • 在清单中添加FileProvider:

  • 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>
    

  • 准备将文件下载到应用程序拥有的任何目录,例如getFilesDir(),getExternalFilesDir(),getCacheDir()或getExternalCacheDir().

    Prepare to download files to any directory your app owns, such as getFilesDir(), getExternalFilesDir(), getCacheDir() or getExternalCacheDir().

    val privateDir = context.getFilesDir()
    

  • 下载文件时要考虑其进度(DIY):

  • Download file taking its progress into account (DIY):

    val downloadedFile = myFancyMethodToDownloadToAnyDir(url, privateDir, fileName)
    

  • 下载后,您可以对文件进行任何威胁.

  • Once downloaded you can make any threatment to the file if you'd like to.

    将其复制到下载"文件夹:

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

  • 在下载文件夹中,您可以像这样从应用程序打开文件:

  • 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"
    

  • 优点:

    • 下载的文件可保留到应用程序卸载

    • 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权限,仅出于此目的:

    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" />
    

    缺点:

    • 在清除应用程序数据或再次卸载并重新安装之后,您将无权访问下载的文件(除非您征得许可,否则它们将不再属于您的应用程序)
    • 设备必须具有更多的可用空间,才能将每个文件从其原始目录复制到其最终目的地.这对于大型文件尤其重要.尽管如果您有权访问原始的InputStream,则可以直接写入downloadedUri,而不用从中间文件进行复制.

    如果这种方法足以满足您的需要,请尝试一下.

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

    这篇关于如何直接将文件下载到Android Q(Android 10)的下载目录中的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆