在Android Q中向公共可访问文档文件夹写入文件 - Scoped Storage

12

背景

迁移到 Android Q 后,我无法找到适当的方法来获得对文档文件夹 (/storage/emulated/0/Documents) 的写访问权限。

在有关范围存储的许多其他问题被提及之前,我已经阅读了很多这方面的问题,并且从目前为止我所看到的所有解决方案都只使用应用程序专用目录或仅访问媒体目录(而不是文档文件夹)。

据我所知,在 Android Q 中,我可以选择:

  • 使用仅可由我的应用程序(或我授权的应用程序)访问的应用程序专用目录
  • 允许用户选择他们想要存储文件的位置(我认为这可以是公共位置)ACTION_OPEN_DOCUMENT_TREE

问题

我开发的应用程序用于对受试者进行测试,测试数据会自动存储在公共可访问的文档文件夹中,类似于:/storage/emulated/0/Documents/myAppName/subjectName/testData-todaysDate.pdf

当用户希望访问测试数据时,他们将智能手机连接到计算机并导航到文档文件夹,其余部分很明显。出于这个原因,我必须使用公共可访问存储。此外,想象一下他们希望通过另一个应用程序在手机上打开测试数据,也是同样的情况!

我正在寻找的解决方案

因此,我正在寻找的解决方案需要能够执行以下操作:

  • 不会多次要求权限(一次就可以)
  • 自动保存测试数据,无需用户提示(因为会进行数百次测试)
  • 保存到公共文档文件夹中,例如/storage/emulated/0/Documents/
  • 不需要用户选择目录,因此不使用ACTION_OPEN_DOCUMENT_TREE
  • 最好是数据持久化,这样卸载应用程序不会导致数据丢失

我知道 Android Q 中的这些变化旨在让用户更了解应用程序如何访问他们的数据,但一旦他们理解了您的应用程序如何使用数据,就不应该存在问题。


1
使用ACTION_OPEN_DOCUMENT_TREE,他们可以在设置应用程序时选择公共位置,然后我们可以永久重复使用该位置。这可能是最好的解决方案,但仍然存在风险,因为用户可能会选择错误的位置,但总比没有好。 - Jameson
当用户想要访问测试数据时,他们将智能手机插入计算机并导航到文档文件夹。这在Android Q设备上是否仍然可行?请报告,因为我没有这样的设备。 - blackapps
您可以让MediaStore将文件写入/storage/emulated/0/Documents、/storage/emulated/0/Download、/storage/emulated/0/Pictures和/storage/emulated/0/DCIM,无需任何权限。 - blackapps
2
"使用ACTION_OPEN_DOCUMENT_TREE,他们可以在设置应用程序时选择公共位置,然后我们可以永久重复使用它吗?"-- 是的。" - CommonsWare
请查看 https://dev59.com/2Lroa4cB1Zd3GeqPs94b#61886002 - Shahbaz Hashmi
2个回答

18

仅适用于Android Q。

String collection = "content://media/1c11-2404/file"; // One can even write to micro SD card
String relative_path = "Documents/MyFolder";

Uri collectionUri = Uri.parse(collection);

ContentValues contentValues = new ContentValues();

contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
contentValues.put(MediaStore.MediaColumns.SIZE, filesize);
contentValues.put(MediaStore.MediaColumns.DATE_MODIFIED, modified );
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, relative_path);
contentValues.put(MediaStore.MediaColumns.IS_PENDING, 1)

Uri fileUri = context.getContentResolver().insert(collectionUri, contentValues);

拥有一个 URI 后,可以打开一个输出流来编写文件的内容。


2
@blackapps:好的,我确认这个方法是可行的。对于外部存储,你可以使用 content://media/external/file(也可以通过 MediaStore.Files.getContentUri("external") 获取)。对于可移动存储,你需要使用存储 ID(在你的代码片段中是 1c11-2404)。我对这个方法的有效性持怀疑态度,因此如果在未来的 Android 版本中出现问题,我不会感到惊讶。但是,这是一个非常好的发现 - 感谢你的分享! - CommonsWare
好的,我有点困惑,在此之前我是这样使用FileOutputStream的:outputStream = new FileOutputStream(filePath, false); 然后我只需 outputStream.write(dataToWrite.getBytes()); 然后 .flush().close()。当然,这仅适用于File,所以我不确定您建议如何使用uri打开输出流? - Jameson
2
OutputStream is = getContentResolver().openOutputStream(fileUri); 输出流is = getContentResolver().openOutputStream(fileUri); - blackapps
1
好的,在@blackapps和@CommonsWare的帮助下,我成功地拼凑出了足够的东西来让它工作。这绝对是非常棒的,但正如其他人所说,我不确定这是否是有意为之的。另外值得注意的是,android:requestLegacyExternalStorage="true"将会持久存在(即使被删除),除非你清除应用程序的数据/缓存。 - Jameson
关于SD卡的UUID,我发现它应该是小写的,与StorageVolume getUuid()返回的相反。 - Thibault

0

尝试将此行放入Android Q的清单文件中:

    android:requestLegacyExternalStorage="true"

在应用程序标签中。


3
这种黑客方法在Q版本之后将无法使用。 - blackapps

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