当前位置: 首页 > 知识库问答 >
问题:

如何直接下载文件到Android Q(Android 10)上的下载目录

益稳
2023-03-14

比如说,我正在开发一个聊天应用程序,可以与其他人共享任何类型的文件(没有mimetype限制):比如图像、视频、文档,但也可以共享压缩文件,比如zip、rar、apk,或者更不常见的文件类型,比如photoshop或autocad文件。

在Android9或更低版本中,我直接将这些文件下载到下载目录,但在Android10中,如果不向用户显示下载它们的意图,就不可能做到这一点。。。

不可能的但为什么谷歌浏览器或其他浏览器能够做到这一点呢?事实上,在Android10中,他们仍然下载文件到下载目录,而不需要询问用户。

我首先分析了Whatsapp以了解它们是如何实现的,但它们利用了AndroidManifest上的requestLegacyExternalStorage属性。但后来我分析了Chrome,它针对的是Android 10,没有使用requestLegacyExternalStorage。这怎么可能?

我已经在谷歌上搜索了好几天了,应用程序可以直接下载一个文件到Android 10(Q)上的下载目录,而不必像Chrome那样询问用户把它放在哪里。

我已经阅读了android for developers文档,关于Stackoverflow的很多问题,互联网上的博客帖子和谷歌群组,但我仍然没有找到一种方法来保持与android 9完全相同的功能,甚至没有找到一个让我满意的解决方案

到目前为止,我一直在尝试:

>

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

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

请求用户许可是一种多么丑陋的行为,不是吗?尽管这不是谷歌Chrome的功能。

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

我还试图获得MediaStore。下载。INTERNAL_CONTENT_URI或MediaStore。下载。EXTERNAL_CONTENT_URI并使用Context.getContentResolver.insert()保存文件,但巧合的是,尽管它们被注释为@NonNull,但它们实际上返回...空

添加requestLegacyExternalStorage=“false”作为我的AndroidManifest的应用程序标签的属性。xml。但这只是开发人员的一个补丁,以便在他们做出更改和修改代码之前争取时间。除此之外,谷歌Chrome并不是这么做的。

getFilesDir()和getExtranalFilesDir()和getExtranalFilesDir()仍然可用,但当我的应用程序卸载时,存储在这些目录上的文件将被删除。用户希望在卸载我的应用程序时保留他们的文件。同样对我来说不是一个可行的解决方案。

我的临时解决方案:

我找到了一种解决方法,可以在不添加requestLegacyExternalStorage=“false”的情况下随时随地下载。

它包括通过以下方式从文件对象获取Uri:

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

有一个提供者。xml

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

并将其设置为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/simulated/0/Download/),并继续创建一个文件对象,但如果谷歌决定更改Android 11上的下载目录路径呢?

恐怕这不是长久之计,所以

我的问题是:

如何在不使用已弃用的方法的情况下实现这一点?还有一个额外的问题,谷歌Chrome如何访问下载目录?

共有3个答案

龙弘盛
2023-03-14

我认为最好用java编写答案,因为有很多遗留代码需要更新。我也不知道你想存储什么样的文件,所以,在这里,我举了一个位图的例子。如果你想保存任何其他类型的文件,你只需要创建一个OutputStream,对于它的剩余部分,你可以按照下面我写的代码。现在,在这里,这个函数只处理Android 10的保存,以及注释。我希望你对如何在较低的Android版本中保存文件有必要的了解

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

现在,如果你想创建一个子目录,即下载文件夹中的文件夹,你可以将其添加到contentValues中

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

这将创建一个名为“folder Name”的文件夹,并在该文件夹中存储“Test.jpg”。

另外请注意,这不需要清单中的任何权限,这意味着它也不需要运行时权限。如果你想要静态编程语言版本,请在评论中问我。我很乐意为它提供静态编程语言方法。

高展
2023-03-14

你可以使用Android下载管理器。请求。在Android 9之前,它将需要写入外部存储任务。从Android 10/Q及以上版本来看,它不需要任何权限(似乎它自己处理权限)。

如果你想在事后打开该文件,你需要获得用户的许可(如果你只想在外部应用程序(例如PDF阅读器)中打开该文件,也需要用户的许可)。

您可以这样使用下载管理器:

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

这是参考:https://developer.android.com/reference/kotlin/android/app/DownloadManager.Request?hl=en#setDestinationInExternalPublicDir(科特林。弦乐Kotlin字符串)

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

翟卓君
2023-03-14

10个多月过去了,但没有给我一个满意的答案。所以我会回答我自己的问题。

正如@commonware在评论中所说,“获取MediaStore.Downloads.INTERNAL_CONTENT_URI或MediaStore.Downloads.EXTERNAL_CONTENT_URI并使用Context.getContentResolver.insert()保存文件”应该是解决方案。我仔细检查了一下,发现这是真的,我说它不起作用是错的。但是

我发现使用ContentResolver很棘手,我无法让它正常工作。我会提出一个单独的问题,但我一直在调查,并找到了一个令人满意的解决方案

我的解决方案:

基本上,你必须下载到你的应用拥有的任何目录,然后复制到下载文件夹。

>

  • 配置你的应用程序:

    >

  • 添加提供者路径。xml到xml资源文件夹

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

    在您的清单中添加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()。

    val privateDir = context.getFilesDir()
    

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

    val downloadedFile = myFancyMethodToDownloadToAnyDir(url, privateDir, fileName)
    

    下载后,如果愿意,可以对文件进行任何威胁。

    复制到下载文件夹:

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

    进入下载文件夹后,您可以从应用程序中打开文件,如下所示:

    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"
    

    优点:

    >

    还允许您在下载时了解其进度

    一旦移动,你仍然可以从应用程序中打开它们,因为文件仍然属于你的应用程序。

    Android Q不需要写入外部存储权限,仅用于此目的:

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

    缺点:

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

    如果这种方法对你来说足够了,那就试一试。

  •  类似资料:
    • 我正在尝试使用Selenium WebDriver自动执行文件下载功能。我正在使用谷歌浏览器,要下载的文件类型是PDF格式。当 WebDriver 单击下载(或打印)链接时,浏览器将显示 pdf 文件的预览,而不是直接下载。如何使chrome驱动程序直接下载pdf文件?我尝试了下面的代码,但没有运气 我知道这个问题已经在StackOverflow上问过了,包括这个,但这些解决方案都不适合我。 我正

    • 希望有人能帮我, 场景:我是, 导航到有下载图标的页面 单击“下载”图标 .pdf文件开始下载到我的项目目录 注意:下载过程中不会显示确认消息,只要单击下载图标,文件就会被下载 观察到:当在本地运行测试用例时(从mvn命令和testng命令),它似乎正在工作并将文件下载到我的目录中。 然而,同样的案例似乎不适用于詹金斯。该文件不会下载到 Jenkins Directory。 本地: Windows

    • 在我实现了一个Android应用程序的解决方案后,发布到web服务器并验证Google Order,然后发布一个下载链接。现在,我正在尝试编写一个应用程序的代码,以读取thankyou.php页面中的链接 该文件是一个“.dat”扩展名,来自某个链接。应用程序应该检索一个新页面中的链接,并让客户下载文件。

    • 问题内容: 目的是从Internet下载文件,并从中创建文件对象或类似文件的文件,而无需使其接触硬盘驱动器。这仅是出于我的知识,想知道它是否可能或可行,尤其是因为我想看看是否可以绕过必须编写文件删除行的代码。 通常,这就是我从网络上下载内容并将其映射到内存的方式: 问题答案: 这就是我最终要做的。

    • 下载 <?php /** * 下载文件请求示例 */ require dirname(__DIR__) . '/vendor/autoload.php'; use Yurun\Util\HttpRequest; use Yurun\Util\YurunHttp; use Yurun\Util\YurunHttp\Handler\Swoole; $url = 'http://www.baid

    • 问题内容: 我刚刚开始学习来自Android的iOS的Apple Swift编程。我现在基本上可以阅读和操作swift代码,还学习了iOS swift编程中使用的一些通用类,但是仍然对语法和所有内容感到困惑。 我正在尝试下载文件。就像,让我们说来自这个网址 在一个按钮中单击。也许也有视觉进步 通过在stackoverflow中搜索,我偶然发现了Alamofire。我可以尝试一下,但是我不确定这是否