只有当用户使用存储访问框架选择文件夹时,我们才会获得树 URI。文档提到从 Android 11 开始,我们可以使用原始文件路径来访问文件。我需要这个,以便我可以使用现有的本机库。但是我找不到任何关于在给定树 URI 的情况下获取原始路径的文档。如何在 Android 11 上获取给定树 URI 的文件路径?
请注意,stackoverflow 上的所有现有答案都是 hack,它们现在甚至都不起作用。我需要一个官方的 API。
只有当用户使用存储访问框架选择文件夹时,我们才会获得树 URI。文档提到从 Android 11 开始,我们可以使用原始文件路径来访问文件。我需要这个,以便我可以使用现有的本机库。但是我找不到任何关于在给定树 URI 的情况下获取原始路径的文档。如何在 Android 11 上获取给定树 URI 的文件路径?
请注意,stackoverflow 上的所有现有答案都是 hack,它们现在甚至都不起作用。我需要一个官方的 API。
对于 API >= 29,存储访问选项发生了巨大变化,由于限制、所需权限等,即使有文件路径也没有多大用处。
API 30 (Andorid 11) 的文档说明可以使用较旧的 File API,指的是这样的 API 现在可以用于访问/处理位于“共享存储”文件夹中的文件,因此 DCIM、音乐、等等。在这些位置之外,文件 API 将无法工作,在 Android 10 中也不会。
请注意,Android 11 和所有未来版本中的 File API 速度要慢得多,因为已被重写为包装器,因此不再是 File system API。Underneath 现在将所有操作委托给 MediaStore。如果性能很重要,那么最好直接使用 MediaStore API。
我建议仅通过ContentResolver使用Uris / File Descriptors,并远离真实的文件路径。对于每个新的 Android 版本,任何解决方法都将是暂时的并且更难维护。
下一个示例(有点 hacky)可以帮助解析 API 29 和 30 中的文件路径;未在 29 以下测试。不是官方的,因此无法保证它适用于所有用例,我建议不要将其用于生产目的。在我的测试中,解析的路径与MediaScannerConnection可以正常工作,但它可能需要调整以处理其他要求。
@Nullable
public static File getFile(@NonNull final Context context, @NonNull final DocumentFile document)
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
{
return null;
}
try
{
final List<StorageVolume> volumeList = context
.getSystemService(StorageManager.class)
.getStorageVolumes();
if ((volumeList == null) || volumeList.isEmpty())
{
return null;
}
// There must be a better way to get the document segment
final String documentId = DocumentsContract.getDocumentId(document.getUri());
final String documentSegment = documentId.substring(documentId.lastIndexOf(':') + 1);
for (final StorageVolume volume : volumeList)
{
final String volumePath;
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q)
{
final Class<?> class_StorageVolume = Class.forName("android.os.storage.StorageVolume");
@SuppressWarnings("JavaReflectionMemberAccess")
@SuppressLint("DiscouragedPrivateApi")
final Method method_getPath = class_StorageVolume.getDeclaredMethod("getPath");
volumePath = (String)method_getPath.invoke(volume);
}
else
{
// API 30
volumePath = volume.getDirectory().getPath();
}
final File storageFile = new File(volumePath + File.separator + documentSegment);
// Should improve with other checks, because there is the
// remote possibility that a copy could exist in a different
// volume (SD-card) under a matching path structure and even
// same file name, (maybe a user's backup in the SD-card).
// Checking for the volume Uuid could be an option but
// as per the documentation the Uuid can be empty.
final boolean isTarget = (storageFile.exists())
&& (storageFile.lastModified() == document.lastModified())
&& (storageFile.length() == document.length());
if (isTarget)
{
return storageFile;
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
return null;
}
如何在 Android 11 上获取给定树 URI 的文件路径?
对不起,但这不是一个选择。
我需要这个,以便我可以使用现有的本机库
让本机库使用您的应用可以直接在 Android 10+ 和旧设备(例如,、)上使用的文件系统getFilesDir()
位置getExternalFilesDir()
。
文档提到从 Android 11 开始,我们可以使用原始文件路径来访问文件。
对,但是:
ACTION_OPEN_DOCUMENT_TREE
不涉及存储访问框架(例如)getDirectory()
您可以使用类似on的APIStorageVolume
通过 Android 11 的“所有文件访问”来处理存储卷(外部和可移动)。有关更多信息,请参阅此博客文章。
您可以使用SimpleStorage:
val file = DocumentFileCompat.fromUri(context, treeUri);
file.absolutePath // => e.g. /storage/emulated/0/MyFolder/MyFile.mp4
file.basePath // => e.g. MyFolder/MyFile.mp4
在我用 androidSdkTarget:29 编译的应用程序上,我可以共享生成的 pdf,在 Android 10 和 11 上,将文件保存在此路径中:
val workSpaceDir = if (isExternalStorageWritable) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q){
File(Globals.application.getExternalFilesDir(null))
} else {
File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).path + "/" + Globals.application.getString(R.string.app_name))
}
} else {
File(Globals.application.filesDir)
}
目前Environment.getExternalStoragePublicDirectory
它已被弃用,但仍然可以工作。这是我找到的唯一方法。
我对我的 android 清单具有以下权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
这里我如何使用它:
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(
Uri.fromFile(workSpaceDir + filename),
Constants.ContentType.ApplicationPdf
)
intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
try {
Globals.currentActivity.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(
Globals.currentActivity,
"No PDF viewer installed.",
Toast.LENGTH_LONG
).show()
}
fun getFilePath(context: Context, contentUri: Uri): String? {
try {
val filePathColumn = arrayOf(
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.TITLE,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATE_ADDED,
MediaStore.Files.FileColumns.DISPLAY_NAME,
)
val returnCursor = contentUri.let { context.contentResolver.query(it, filePathColumn, null, null, null) }
if (returnCursor != null) {
returnCursor.moveToFirst()
val nameIndex = returnCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
val name = returnCursor.getString(nameIndex)
val file = File(context.cacheDir, name)
val inputStream = context.contentResolver.openInputStream(contentUri)
val outputStream = FileOutputStream(file)
var read: Int
val maxBufferSize = 1 * 1024 * 1024
val bytesAvailable = inputStream!!.available()
val bufferSize = min(bytesAvailable, maxBufferSize)
val buffers = ByteArray(bufferSize)
while (inputStream.read(buffers).also { read = it } != -1) {
outputStream.write(buffers, 0, read)
}
inputStream.close()
outputStream.close()
return file.absolutePath
}
else
{
Log.d("","returnCursor is null")
return null
}
}
catch (e: Exception) {
Log.d("","exception caught at getFilePath(): $e")
return null
}
}
SimpleStorage无法在真实设备上运行(Samsung Galaxy Tab S7 FE Android 11 (R)),但它适用于模拟器。无论如何,我修改了这段代码,现在它工作得很好。
它在缓存中创建文件,这就是它与 Android 11 完美配合的原因。
我正在通过此功能选择 PDF。然后我在 onActivityResult 或 ActivityResultLauncher 中传递选定的 uri。
fun pickPDF(activityResult: ActivityResultLauncher<Intent>){
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "application/pdf"
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
activityResult.launch(intent)
}
pdfPickerResult = registerForActivityResult(StartActivityForResult()) { result: ActivityResult? ->
if (result != null) {
val intent = result.data
if (intent != null) {
val uri = intent.data
if (uri != null) {
requireContext().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
val path = FileFinder.getFilePath(requireContext(),uri)
if (path != null && path.length > 3)
{
//Do your implementation.
}
else
{
Log.d("","PDF's path is null")
}
}
}
}
}
所以最终我们在 Android 11 上从 Uri 获取 Filepath。如果你想对图像等做同样的事情,你必须使用 filePathColumn,intent 值。
权限呢?
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
在清单文件中添加这些标志。
之后,我们需要在运行时请求许可。
fun checkAndRequestPermissions(context: Activity): Boolean {
val extStorePermission = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE)
val cameraPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
val mediaLocationPermission = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_MEDIA_LOCATION)
val listPermissionsNeeded: MutableList<String> = ArrayList()
if (cameraPermission != PackageManager.PERMISSION_GRANTED) {
listPermissionsNeeded.add(Manifest.permission.CAMERA)
}
if (extStorePermission != PackageManager.PERMISSION_GRANTED) {
listPermissionsNeeded.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
if (extStorePermission != PackageManager.PERMISSION_GRANTED) {
listPermissionsNeeded.add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
if (mediaLocationPermission != PackageManager.PERMISSION_GRANTED) {
listPermissionsNeeded.add(Manifest.permission.ACCESS_MEDIA_LOCATION)
}
if (extStorePermission != PackageManager.PERMISSION_GRANTED) {
listPermissionsNeeded.add(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
}
if (listPermissionsNeeded.isNotEmpty()) {
ActivityCompat.requestPermissions(context, listPermissionsNeeded.toTypedArray(), REQUEST_ID_MULTIPLE_PERMISSIONS)
return false
}
return true
}
您可以使用此功能。但我们还没有完成。我们仍然没有完全访问权限。
我们将使用该功能将用户重定向到应用程序设置。
private fun requestAllFilesAccessPermission(){
if (!Environment.isExternalStorageManager()) {
Snackbar.make(findViewById(android.R.id.content), "Permissin Needed", Snackbar.LENGTH_INDEFINITE)
.setAction("Settings") {
try {
val uri: Uri = Uri.parse("package:" + BuildConfig.APPLICATION_ID)
val intent =
Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, uri)
startActivity(intent)
} catch (ex: Exception) {
val intent = Intent()
intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
startActivity(intent)
}
}.show()
}
}