Как получить путь к файлу с учетом URI дерева на Android 11?

Мы получаем URI дерева только тогда, когда пользователь выбирает папку, используя структуру доступа к хранилищу. В документации упоминается, что начиная с Android 11 мы можем использовать необработанные пути к файлам для доступа к файлам. Мне это нужно, чтобы я мог использовать существующую собственную библиотеку. Но я не могу найти никакой документации по получению необработанного пути по URI дерева. Как получить путь к файлу с учетом URI дерева на Android 11?

Обратите внимание, что все существующие ответы на stackru - это взломы, и теперь они даже не работают. Для этого мне нужен официальный API.

5 ответов

Для API >= 29 параметры доступа к хранилищу изменились настолько радикально, что даже наличие пути к файлу не будет иметь большого значения из-за ограничений, необходимых разрешений и т. д.

Документация для API 30 (Andorid 11), в которой говорится, что можно использовать более старый File API, ссылается на тот факт, что такой API теперь можно использовать для доступа/работы с файлами, расположенными в папках «общего хранилища», поэтому DCIM, Music, и т. д. Вне этих мест файловый API не будет работать ни в Android 10.

Обратите внимание, что File API в Android 11 и во всех будущих версиях работает намного медленнее со штрафным ударом , потому что он был переписан в оболочку, поэтому больше не является API файловой системы. Внизу теперь делегирует все операции в MediaStore. Если важна производительность, то лучше использовать непосредственно MediaStore API.

Я бы рекомендовал работать только с Uris / File Descriptors через ContentResolver и уйти от реальных путей к файлам. Любой обходной путь будет временным, и его будет сложнее поддерживать с каждой новой версией Android.

Следующий пример (немного хакерский) может помочь разрешить путь к файлу в 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;
}
      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 через эту функцию. Затем я передаю выбранный uri в onActivityResult или ActivityResultLauncher.

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

Итак, в конечном итоге мы получаем Filepath от Uri на Android 11. Если вы хотите сделать то же самое для изображения и т. д., вам нужно поиграть с 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()
    }
}

Вы можете с 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()
}

Как получить путь к файлу с учетом URI дерева на Android 11?

Извините, но это не вариант.

Мне это нужно, чтобы я мог использовать существующую собственную библиотеку

Собственная библиотека должна работать с расположением файловой системы, которое ваше приложение может использовать непосредственно на Android 10+ и на старых устройствах (например, getFilesDir(), getExternalFilesDir()).

В документации упоминается, что начиная с Android 11 мы можем использовать необработанные пути к файлам для доступа к файлам.

Да, но:

  • Это не будет работать на Android 10 и
  • Платформа доступа к хранилищу (например, ACTION_OPEN_DOCUMENT_TREE) не участвует

Вы бы использовали такие API, как getDirectory() на StorageVolumeдля работы с дисковыми томами (внешними и съемными) через "доступ ко всем файлам" Android 11. См. Это сообщение в блоге для получения дополнительной информации.

Другие вопросы по тегам