Google은 흑인 공동체를 위한 인종 간 평등을 진전시키기 위해 노력하고 있습니다. Google에서 어떤 노력을 하고 있는지 확인하세요.

공유 저장소의 문서 및 기타 파일 액세스

Android 4.4(API 수준 19) 이상을 실행하는 기기에서 앱은 저장소 액세스 프레임워크를 사용하여 외부 저장소 볼륨 및 클라우드 기반 저장소를 포함한 문서 제공업체와 상호작용할 수 있습니다. 이 프레임워크를 통해 사용자는 시스템 선택도구와 상호작용하여 문서 제공업체를 선택하고 앱에서 만들거나 열거나 수정할 특정 문서 및 기타 파일을 선택할 수 있습니다.

사용자가 앱이 액세스할 수 있는 파일 또는 디렉터리를 선택해야 하므로 이 메커니즘에서는 시스템 권한이 필요하지 않으며 사용자 제어 및 개인정보 보호가 강화됩니다. 또한 앱별 디렉터리 외부 및 미디어 저장소 외부에 저장된 이 파일들은 앱을 제거한 후에도 기기에 남아 있습니다.

프레임워크 사용에는 다음 단계가 포함됩니다.

  1. 앱이 저장소 관련 작업이 포함된 인텐트를 호출합니다. 이 작업은 프레임워크에서 제공하는 특정 사용 사례에 상응합니다.
  2. 사용자는 시스템 선택도구를 확인하여 문서 제공업체를 탐색하고 저장소 관련 작업이 발생하는 위치 또는 문서를 선택할 수 있습니다.
  3. 앱은 사용자가 선택한 위치 또는 문서를 나타내는 URI의 읽기 및 쓰기 액세스 권한을 얻습니다. 앱은 이 URI를 사용하여 선택된 위치에서 작업을 실행할 수 있습니다.

그러나 앱이 미디어 저장소를 사용한다면 다른 앱의 미디어 파일에 액세스할 수 있는 READ_EXTERNAL_STORAGE 권한을 요청해야 합니다. Android 9(API 수준 28) 이하를 실행하는 기기에서 앱이 자체적으로 생성한 미디어 파일을 비롯하여 모든 미디어 파일에 액세스하려면 READ_EXTERNAL_STORAGE 권한을 요청해야 합니다.

이 가이드에서는 프레임워크가 파일 및 기타 문서 작업을 지원하는 다양한 사용 사례를 설명합니다. 또한 사용자가 선택한 위치에서 작업을 실행하는 방법도 설명합니다.

문서 및 기타 파일에 액세스하기 위한 사용 사례

저장소 액세스 프레임워크는 파일 및 기타 문서에 액세스하기 위한 다음 사용 사례를 지원합니다.

새 파일 만들기
ACTION_CREATE_DOCUMENT 인텐트 작업을 통해 사용자는 파일을 특정 위치에 저장할 수 있습니다.
문서 또는 파일 열기
ACTION_OPEN_DOCUMENT 인텐트 작업을 통해 사용자는 특정 문서 또는 파일을 선택하여 열 수 있습니다.
디렉터리의 콘텐츠에 관한 액세스 권한 부여
Android 5.0(API 수준 21) 이상에서 사용 가능한 ACTION_OPEN_DOCUMENT_TREE 인텐트 작업을 통해 사용자는 특정 디렉터리를 선택하여 이 디렉터리 내의 모든 파일 및 하위 디렉터리에 관한 액세스 권한을 앱에 부여할 수 있습니다.

다음 섹션에서는 각 사용 사례를 구성하는 방법을 안내합니다.

새 파일 만들기

ACTION_CREATE_DOCUMENT 인텐트 작업을 사용하여 시스템 파일 선택기를 로드하고 사용자가 파일의 콘텐츠를 쓸 위치를 선택할 수 있도록 합니다. 이 프로세스는 다른 운영체제에서 사용하는 '다른 이름으로 저장' 대화상자에서 사용되는 프로세스와 유사합니다.

참고: ACTION_CREATE_DOCUMENT는 기존 파일을 덮어쓸 수 없습니다. 앱이 동일한 이름으로 파일을 저장하려고 하면 시스템은 파일 이름 끝에 괄호로 묶은 숫자를 추가합니다.

예를 들어 앱이 confirmation.pdf라는 이름의 파일이 이미 있는 디렉터리에 이 이름을 사용해 파일을 저장하려고 하면 시스템은 새 파일을 confirmation.pdf (1)이라는 이름으로 저장합니다.

인텐트를 구성할 때 파일의 이름 및 MIME 유형을 지정하고 선택적으로 EXTRA_INITIAL_URI 인텐트 extra를 사용하여 파일 선택기가 처음 로드될 때 표시해야 하는 파일 또는 디렉터리의 URI를 선택적으로 지정합니다.

다음 코드 스니펫은 파일을 만들기 위한 인텐트를 생성 및 호출하는 방법을 보여줍니다.

Kotlin

// 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 유형을 지정합니다. 또한 EXTRA_INITIAL_URI 인텐트 extra를 사용하여 파일 선택기가 처음 로드될 때 표시해야 하는 파일의 URI를 선택적으로 지정할 수 있습니다.

다음 코드 스니펫은 PDF 문서를 열기 위한 인텐트를 생성 및 호출하는 방법을 보여줍니다.

Kotlin

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

디렉터리의 콘텐츠에 관한 액세스 권한 부여

파일 관리 앱과 미디어 제작 앱은 일반적으로 디렉터리 계층 구조로 파일 그룹을 관리합니다. 앱에서 이 기능을 제공하려면 ACTION_OPEN_DOCUMENT_TREE 인텐트 작업을 사용합니다. 이를 통해 사용자가 디렉터리 트리 전체에 관한 액세스 권한을 부여할 수 있습니다. 그러면 앱은 선택된 디렉터리 및 그 하위 디렉터리에 있는 모든 파일에 액세스할 수 있습니다.

ACTION_OPEN_DOCUMENT_TREE를 사용하면 앱은 사용자가 선택한 디렉터리의 파일에만 액세스할 수 있습니다. 사용자가 선택한 이 디렉터리 외부에 있는 다른 앱의 파일에는 액세스할 수 없습니다. 이러한 사용자 제어 액세스를 통해 사용자는 앱과 편히 공유할 수 있는 콘텐츠를 정확히 선택할 수 있습니다.

선택적으로 EXTRA_INITIAL_URI 인텐트 extra를 사용하여 파일 선택기가 처음 로드될 때 표시해야 하는 디렉터리의 URI를 지정할 수 있습니다.

다음 코드 스니펫은 디렉터리를 열기 위한 인텐트를 생성 및 호출하는 방법을 보여줍니다.

Kotlin

fun openDirectory(pickerInitialUri: Uri) {
    // Choose a directory using the system's file picker.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
        // Provide read access to files and sub-directories in the user-selected
        // directory.
        flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

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

    // Provide read access to files and sub-directories in the user-selected
    // directory.
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

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

선택된 위치에서 작업 실행

사용자가 시스템의 파일 선택기를 사용하여 파일 또는 디렉터리를 선택하면 앱은 onActivityResult()에서 다음 코드를 사용하여 선택된 항목의 URI를 검색할 수 있습니다.

Kotlin

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의 값을 확인합니다. 그러면 앱의 UI는 제공업체가 지원하는 옵션만 표시할 수 있습니다.

권한 유지

앱이 읽기 또는 쓰기 작업을 위해 파일을 열면 시스템이 앱에 파일의 URI 권한을 부여합니다. 이는 사용자가 기기를 다시 시작할 때까지 지속됩니다. 그러나 앱이 이미지 편집 앱인데 사용자가 가장 최근에 편집한 5개의 이미지에 이 앱에서 직접 액세스할 수 있어야 한다고 가정해 보겠습니다. 사용자의 기기가 다시 시작되면 사용자를 다시 시스템 선택도구로 보내 파일을 찾도록 해야 합니다.

기기가 다시 시작될 때 파일에 관한 액세스를 유지하고 더 나은 사용자 환경을 구현하기 위해 앱은 다음 코드 스니펫과 같이 시스템에서 제공하는 유지 가능한 URI 권한 부여를 '받을' 수 있습니다.

Kotlin

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로 지정된 문서의 메타데이터를 가져와서 로깅합니다.

Kotlin

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 참조가 있으면 추가 처리를 위해 문서를 열 수 있습니다. 이 섹션에서는 비트맵 및 입력 스트림을 여는 예를 보여줍니다.

비트맵

다음 코드 스니펫은 URI가 지정된 Bitmap 파일을 여는 방법을 보여줍니다.

Kotlin

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에 표시할 수 있습니다.

입력 스트림

다음 코드 스니펫은 URI가 지정된 InputStream 객체를 여는 방법을 보여줍니다. 이 스니펫에서는 파일의 행을 문자열로 읽습니다.

Kotlin

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

문서 편집

저장소 액세스 프레임워크를 사용하여 준비된 텍스트 문서를 편집할 수 있습니다.

다음 코드 스니펫은 지정된 URI로 표시되는 문서의 콘텐츠를 덮어씁니다.

Kotlin

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_FLAGSSUPPORTS_DELETE가 포함되어 있다면 문서를 삭제할 수 있습니다. 예:

Kotlin

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)

자바

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);

가상 파일 열기

Android 7.0(API 수준 25) 이상에서는 앱이 저장소 액세스 프레임워크에서 제공하는 가상 파일을 활용할 수 있습니다. 가상 파일에 바이너리 표현이 없더라도 앱은 다른 파일 형식으로 강제로 변환하거나 ACTION_VIEW 인텐트 작업을 사용해 파일을 보는 방법으로 콘텐츠를 열 수 있습니다.

가상 파일을 열려면 클라이언트 앱에 이를 처리하기 위한 특수 로직이 포함되어 있어야 합니다. 예를 들어 파일을 미리 보기 위해 파일의 바이트 표현을 얻고 싶다면 문서 제공업체의 대체 MIME 유형을 요청해야 합니다.

사용자가 선택한 이후에 다음 코드 스니펫과 같이 결과 데이터의 URI를 사용하여 파일이 가상인지 확인합니다.

Kotlin

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")으로 강제 변환할 수 있습니다. 다음 코드 스니펫은 가상 파일을 이미지로 표현할 수 있는지 확인하고 표현이 가능할 경우 가상 파일에서 입력 스트림을 가져오는 방법을 보여줍니다.

Kotlin

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

추가 리소스

문서 및 기타 파일을 저장하고 액세스하는 방법에 관한 자세한 내용은 다음 리소스를 참조하세요.

샘플

동영상