Доступ к документам и другим файлам из общего хранилища

На устройствах под управлением Android 4.4 (уровень API 19) и выше ваше приложение может взаимодействовать с поставщиком документов , включая тома внешнего хранилища и облачное хранилище, с помощью Storage Access Framework. Эта платформа позволяет пользователям взаимодействовать со средством выбора системы, чтобы выбирать поставщика документов и выбирать конкретные документы и другие файлы для создания, открытия или изменения вашего приложения.

Поскольку пользователь участвует в выборе файлов или каталогов, к которым может получить доступ ваше приложение, этот механизм не требует каких-либо системных разрешений , а контроль и конфиденциальность пользователей улучшаются. Кроме того, эти файлы, которые хранятся за пределами каталога приложения и за пределами хранилища мультимедиа, остаются на устройстве после удаления вашего приложения.

Использование фреймворка включает в себя следующие шаги:

  1. Приложение вызывает намерение, содержащее действие, связанное с хранилищем. Это действие соответствует конкретному варианту использования , который предоставляет платформа.
  2. Пользователь видит средство выбора системы, позволяющее ему просматривать поставщика документов и выбирать место или документ, где происходит действие, связанное с хранением.
  3. Приложение получает доступ для чтения и записи к URI, который представляет выбранное пользователем местоположение или документ. Используя этот URI, приложение может выполнять операции в выбранном месте .

Чтобы поддерживать доступ к медиафайлам на устройствах под управлением Android 9 (уровень API 28) или ниже, объявите разрешение READ_EXTERNAL_STORAGE и установите для maxSdkVersion значение 28 .

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

Варианты использования для доступа к документам и другим файлам

Платформа доступа к хранилищу поддерживает следующие варианты использования для доступа к файлам и другим документам.

Создать новый файл
Действие намерения ACTION_CREATE_DOCUMENT позволяет пользователям сохранять файл в определенном месте.
Открыть документ или файл
Действие намерения ACTION_OPEN_DOCUMENT позволяет пользователям выбрать конкретный документ или файл для открытия.
Предоставить доступ к содержимому каталога
Действие намерения ACTION_OPEN_DOCUMENT_TREE , доступное в Android 5.0 (уровень API 21) и выше, позволяет пользователям выбирать определенный каталог, предоставляя вашему приложению доступ ко всем файлам и подкаталогам в этом каталоге.

В следующих разделах представлены инструкции по настройке каждого варианта использования.

Создать новый файл

Используйте действие намерения ACTION_CREATE_DOCUMENT , чтобы загрузить средство выбора системных файлов и позволить пользователю выбрать место для записи содержимого файла. Этот процесс аналогичен тому, который используется в диалоговых окнах «Сохранить как», используемых в других операционных системах.

Примечание. ACTION_CREATE_DOCUMENT не может перезаписать существующий файл. Если ваше приложение пытается сохранить файл с тем же именем, система добавляет число в круглых скобках в конце имени файла.

Например, если ваше приложение пытается сохранить файл с именем confirmation.pdf в каталоге, в котором уже есть файл с таким именем, система сохраняет новый файл с именем confirmation(1).pdf .

При настройке намерения укажите имя файла и тип MIME и, при необходимости, укажите URI файла или каталога, который средство выбора файлов должно отображать при первой загрузке, используя дополнительное намерение EXTRA_INITIAL_URI .

В следующем фрагменте кода показано, как создать и вызвать намерение создания файла:

Котлин

// Request code for creating a PDF document.
const val CREATE_FILE = 1

private fun createFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"
        putExtra(Intent.EXTRA_TITLE, "invoice.pdf")

        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker before your app creates the document.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }
    startActivityForResult(intent, CREATE_FILE)
}

Ява

// Request code for creating a PDF document.
private static final int CREATE_FILE = 1;

private void createFile(Uri pickerInitialUri) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("application/pdf");
    intent.putExtra(Intent.EXTRA_TITLE, "invoice.pdf");

    // Optionally, specify a URI for the directory that should be opened in
    // the system file picker when your app creates the document.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);

    startActivityForResult(intent, CREATE_FILE);
}

Открыть файл

Ваше приложение может использовать документы в качестве единицы хранения, в которую пользователи вводят данные, которыми они могут захотеть поделиться с коллегами или импортировать в другие документы. Несколько примеров включают в себя открытие пользователем рабочего документа или открытие книги, сохраненной в виде файла EPUB.

В этих случаях позвольте пользователю выбрать файл для открытия, вызвав намерение ACTION_OPEN_DOCUMENT , которое открывает системное приложение для выбора файлов. Чтобы отображать только те типы файлов, которые поддерживает ваше приложение, укажите тип MIME. Кроме того, вы можете дополнительно указать URI файла, который средство выбора файлов должно отображать при первой загрузке, используя дополнительное намерение EXTRA_INITIAL_URI .

В следующем фрагменте кода показано, как создать и вызвать намерение открыть PDF-документ:

Котлин

// Request code for selecting a PDF document.
const val PICK_PDF_FILE = 2

fun openFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"

        // Optionally, specify a URI for the file that should appear in the
        // system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, PICK_PDF_FILE)
}

Ява

// Request code for selecting a PDF document.
private static final int PICK_PDF_FILE = 2;

private void openFile(Uri pickerInitialUri) {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("application/pdf");

    // Optionally, specify a URI for the file that should appear in the
    // system file picker when it loads.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);

    startActivityForResult(intent, PICK_PDF_FILE);
}

Ограничения доступа

В Android 11 (уровень API 30) и более поздних версиях вы не можете использовать действие намерения ACTION_OPEN_DOCUMENT , чтобы запросить у пользователя выбор отдельных файлов из следующих каталогов:

  • Каталог Android/data/ и все подкаталоги.
  • Каталог Android/obb/ и все подкаталоги.

Предоставить доступ к содержимому каталога

Приложения для управления файлами и создания мультимедиа обычно управляют группами файлов в иерархии каталогов. Чтобы обеспечить эту возможность в вашем приложении, используйте действие намерения ACTION_OPEN_DOCUMENT_TREE , которое позволяет пользователю предоставить доступ ко всему дереву каталогов, за некоторыми исключениями, начиная с Android 11 (уровень API 30). После этого ваше приложение сможет получить доступ к любому файлу в выбранном каталоге и любом из его подкаталогов.

При использовании ACTION_OPEN_DOCUMENT_TREE ваше приложение получает доступ только к файлам в каталоге, который выбирает пользователь. У вас нет доступа к файлам других приложений, которые находятся за пределами выбранного пользователем каталога. Этот управляемый пользователем доступ позволяет пользователям выбирать, каким именно контентом им удобно делиться с вашим приложением.

При желании вы можете указать URI каталога, который средство выбора файлов должно отображать при первой загрузке, используя дополнительное намерение EXTRA_INITIAL_URI .

В следующем фрагменте кода показано, как создать и вызвать намерение открытия каталога:

Котлин

fun openDirectory(pickerInitialUri: Uri) {
    // Choose a directory using the system's file picker.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, your-request-code)
}

Ява

public void openDirectory(Uri uriToLoad) {
    // Choose a directory using the system's file picker.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);

    // Optionally, specify a URI for the directory that should be opened in
    // the system file picker when it loads.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uriToLoad);

    startActivityForResult(intent, your-request-code);
}

Ограничения доступа

В Android 11 (уровень API 30) и более поздних версиях вы не можете использовать действие намерения ACTION_OPEN_DOCUMENT_TREE для запроса доступа к следующим каталогам:

  • Корневой каталог тома внутреннего хранилища.
  • Корневой каталог каждого тома SD-карты, который производитель устройства считает надежным , независимо от того, является ли карта эмулируемой или съемной. Надежный том — это тот, к которому приложение может успешно получить доступ большую часть времени.
  • Каталог Download .

Кроме того, в Android 11 (уровень API 30) и выше вы не можете использовать действие намерения ACTION_OPEN_DOCUMENT_TREE , чтобы запросить у пользователя выбор отдельных файлов из следующих каталогов:

  • Каталог Android/data/ и все подкаталоги.
  • Каталог Android/obb/ и все подкаталоги.

Выполнять операции в выбранном месте

После того, как пользователь выбрал файл или каталог с помощью системного средства выбора файлов, вы можете получить URI выбранного элемента, используя следующий код в onActivityResult() :

Котлин

override fun onActivityResult(
        requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        resultData?.data?.also { uri ->
            // Perform operations on the document using its URI.
        }
    }
}

Ява

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            // Perform operations on the document using its URI.
        }
    }
}

Получив ссылку на URI выбранного элемента, ваше приложение может выполнить с ним несколько операций. Например, вы можете получить доступ к метаданным элемента, отредактировать элемент на месте и удалить его.

В следующих разделах показано, как выполнять действия с файлами, выбранными пользователем.

Определите операции, которые поддерживает поставщик

Различные поставщики контента позволяют выполнять с документами разные операции, например копирование документа или просмотр миниатюры документа. Чтобы определить, какие операции поддерживает данный поставщик, проверьте значение Document.COLUMN_FLAGS . Пользовательский интерфейс вашего приложения сможет отображать только те параметры, которые поддерживает поставщик.

Сохранение разрешений

Когда ваше приложение открывает файл для чтения или записи, система предоставляет вашему приложению разрешение URI для этого файла, которое действует до перезагрузки устройства пользователя. Предположим, однако, что ваше приложение представляет собой приложение для редактирования изображений, и вы хотите, чтобы пользователи могли получить доступ к 5 изображениям, которые они редактировали последними, непосредственно из вашего приложения. Если устройство пользователя перезагрузилось, вам придется отправить пользователя обратно в средство выбора системы, чтобы найти файлы.

Чтобы сохранить доступ к файлам при перезагрузке устройства и улучшить взаимодействие с пользователем, ваше приложение может «принять» разрешение постоянного URI, которое предлагает система, как показано в следующем фрагменте кода:

Котлин

val contentResolver = applicationContext.contentResolver

val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// Check for the freshest data.
contentResolver.takePersistableUriPermission(uri, takeFlags)

Ява

final int takeFlags = intent.getFlags()
            & (Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);

Изучите метаданные документа

Имея URI документа, вы получаете доступ к его метаданным. Этот фрагмент извлекает метаданные документа, указанного URI, и записывает их:

Котлин

val contentResolver = applicationContext.contentResolver

fun dumpImageMetaData(uri: Uri) {

    // The query, because it only applies to a single document, returns only
    // one row. There's no need to filter, sort, or select fields,
    // because we want all fields for one document.
    val cursor: Cursor? = contentResolver.query(
            uri, null, null, null, null, null)

    cursor?.use {
        // moveToFirst() returns false if the cursor has 0 rows. Very handy for
        // "if there's anything to look at, look at it" conditionals.
        if (it.moveToFirst()) {

            // Note it's called "Display Name". This is
            // provider-specific, and might not necessarily be the file name.
            val displayName: String =
                    it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
            Log.i(TAG, "Display Name: $displayName")

            val sizeIndex: Int = it.getColumnIndex(OpenableColumns.SIZE)
            // If the size is unknown, the value stored is null. But because an
            // int can't be null, the behavior is implementation-specific,
            // and unpredictable. So as
            // a rule, check if it's null before assigning to an int. This will
            // happen often: The storage API allows for remote files, whose
            // size might not be locally known.
            val size: String = if (!it.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                it.getString(sizeIndex)
            } else {
                "Unknown"
            }
            Log.i(TAG, "Size: $size")
        }
    }
}

Ява

public void dumpImageMetaData(Uri uri) {

    // The query, because it only applies to a single document, returns only
    // one row. There's no need to filter, sort, or select fields,
    // because we want all fields for one document.
    Cursor cursor = getActivity().getContentResolver()
            .query(uri, null, null, null, null, null);

    try {
        // moveToFirst() returns false if the cursor has 0 rows. Very handy for
        // "if there's anything to look at, look at it" conditionals.
        if (cursor != null && cursor.moveToFirst()) {

            // Note it's called "Display Name". This is
            // provider-specific, and might not necessarily be the file name.
            String displayName = cursor.getString(
                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            Log.i(TAG, "Display Name: " + displayName);

            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            // If the size is unknown, the value stored is null. But because an
            // int can't be null, the behavior is implementation-specific,
            // and unpredictable. So as
            // a rule, check if it's null before assigning to an int. This will
            // happen often: The storage API allows for remote files, whose
            // size might not be locally known.
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                size = cursor.getString(sizeIndex);
            } else {
                size = "Unknown";
            }
            Log.i(TAG, "Size: " + size);
        }
    } finally {
        cursor.close();
    }
}

Открыть документ

Имея ссылку на URI документа, вы можете открыть документ для дальнейшей обработки. В этом разделе показаны примеры открытия растрового изображения и входного потока.

Растровое изображение

В следующем фрагменте кода показано, как открыть файл Bitmap по его URI:

Котлин

val contentResolver = applicationContext.contentResolver

@Throws(IOException::class)
private fun getBitmapFromUri(uri: Uri): Bitmap {
    val parcelFileDescriptor: ParcelFileDescriptor =
            contentResolver.openFileDescriptor(uri, "r")
    val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor
    val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor)
    parcelFileDescriptor.close()
    return image
}

Ява

private Bitmap getBitmapFromUri(Uri uri) throws IOException {
    ParcelFileDescriptor parcelFileDescriptor =
            getContentResolver().openFileDescriptor(uri, "r");
    FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
    Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
    parcelFileDescriptor.close();
    return image;
}

После открытия растрового изображения его можно отобразить в ImageView .

Входной поток

В следующем фрагменте кода показано, как открыть объект InputStream по его URI. В этом фрагменте строки файла считываются в строку:

Котлин

val contentResolver = applicationContext.contentResolver

@Throws(IOException::class)
private fun readTextFromUri(uri: Uri): String {
    val stringBuilder = StringBuilder()
    contentResolver.openInputStream(uri)?.use { inputStream ->
        BufferedReader(InputStreamReader(inputStream)).use { reader ->
            var line: String? = reader.readLine()
            while (line != null) {
                stringBuilder.append(line)
                line = reader.readLine()
            }
        }
    }
    return stringBuilder.toString()
}

Ява

private String readTextFromUri(Uri uri) throws IOException {
    StringBuilder stringBuilder = new StringBuilder();
    try (InputStream inputStream =
            getContentResolver().openInputStream(uri);
            BufferedReader reader = new BufferedReader(
            new InputStreamReader(Objects.requireNonNull(inputStream)))) {
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
        }
    }
    return stringBuilder.toString();
}

Редактировать документ

Вы можете использовать Storage Access Framework для редактирования текстового документа на месте.

Следующий фрагмент кода перезаписывает содержимое документа, представленного данным URI:

Котлин

val contentResolver = applicationContext.contentResolver

private fun alterDocument(uri: Uri) {
    try {
        contentResolver.openFileDescriptor(uri, "w")?.use {
            FileOutputStream(it.fileDescriptor).use {
                it.write(
                    ("Overwritten at ${System.currentTimeMillis()}\n")
                        .toByteArray()
                )
            }
        }
    } catch (e: FileNotFoundException) {
        e.printStackTrace()
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

Ява

private void alterDocument(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten at " + System.currentTimeMillis() +
                "\n").getBytes());
        // Let the document provider know you're done by closing the stream.
        fileOutputStream.close();
        pfd.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Удалить документ

Если у вас есть URI документа и Document.COLUMN_FLAGS документа содержит SUPPORTS_DELETE , вы можете удалить документ. Например:

Котлин

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)

Ява

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);

Получить эквивалентный URI мультимедиа.

Метод getMediaUri() предоставляет URI медиахранилища, эквивалентный URI данного поставщика документов. Два URI относятся к одному и тому же базовому элементу. Используя URI медиахранилища, вы можете упростить доступ к медиафайлам из общего хранилища .

Метод getMediaUri() поддерживает URI ExternalStorageProvider . В Android 12 (уровень API 31) и более поздних версиях метод также поддерживает URI MediaDocumentsProvider .

Открыть виртуальный файл

В Android 7.0 (уровень API 25) и более поздних версиях ваше приложение может использовать виртуальные файлы, которые предоставляет Storage Access Framework. Несмотря на то, что виртуальные файлы не имеют двоичного представления, ваше приложение может открыть их содержимое, переведя их в другой тип файла или просмотрев эти файлы с помощью действия намерения ACTION_VIEW .

Чтобы открывать виртуальные файлы, ваше клиентское приложение должно включать специальную логику для их обработки. Если вы хотите получить байтовое представление файла (например, для предварительного просмотра файла), вам необходимо запросить альтернативный тип MIME у поставщика документов.

После того как пользователь сделает выбор, используйте URI в данных результатов, чтобы определить, является ли файл виртуальным, как показано в следующем фрагменте кода:

Котлин

private fun isVirtualFile(uri: Uri): Boolean {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false
    }

    val cursor: Cursor? = contentResolver.query(
            uri,
            arrayOf(DocumentsContract.Document.COLUMN_FLAGS),
            null,
            null,
            null
    )

    val flags: Int = cursor?.use {
        if (cursor.moveToFirst()) {
            cursor.getInt(0)
        } else {
            0
        }
    } ?: 0

    return flags and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0
}

Ява

private boolean isVirtualFile(Uri uri) {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false;
    }

    Cursor cursor = getContentResolver().query(
        uri,
        new String[] { DocumentsContract.Document.COLUMN_FLAGS },
        null, null, null);

    int flags = 0;
    if (cursor.moveToFirst()) {
        flags = cursor.getInt(0);
    }
    cursor.close();

    return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
}

Убедившись, что документ является виртуальным файлом, вы можете преобразовать его в альтернативный тип MIME, например "image/png" . В следующем фрагменте кода показано, как проверить, может ли виртуальный файл быть представлен в виде изображения, и если да, то получить входной поток из виртуального файла:

Котлин

@Throws(IOException::class)
private fun getInputStreamForVirtualFile(
        uri: Uri, mimeTypeFilter: String): InputStream {

    val openableMimeTypes: Array<String>? =
            contentResolver.getStreamTypes(uri, mimeTypeFilter)

    return if (openableMimeTypes?.isNotEmpty() == true) {
        contentResolver
                .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
                .createInputStream()
    } else {
        throw FileNotFoundException()
    }
}

Ява

private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter)
    throws IOException {

    ContentResolver resolver = getContentResolver();

    String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter);

    if (openableMimeTypes == null ||
        openableMimeTypes.length < 1) {
        throw new FileNotFoundException();
    }

    return resolver
        .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
        .createInputStream();
}

Дополнительные ресурсы

Для получения дополнительной информации о том, как хранить документы и другие файлы и получать к ним доступ, обратитесь к следующим ресурсам.

Образцы

Видео