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

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

사용자가 직접 앱이 액세스할 수 있는 파일이나 디렉터리를 선택하므로 이 메커니즘에서는 시스템 권한을 요청할 필요가 없고 따라서 강화된 사용자 제어와 개인 정보 보호를 제공합니다. 또한 앱별 디렉터리 외부 및 미디어 저장소 외부에 저장된 이 파일들은 앱을 제거한 후에도 기기에 남아 있습니다.

프레임워크 사용 절차는 다음을 포함합니다.

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

Android 9(API 수준 28) 이하를 실행하는 기기에서 미디어 파일 액세스를 지원하려면 READ_EXTERNAL_STORAGE 권한을 선언하고 maxSdkVersion28로 설정하세요.

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

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

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

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

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

새 파일 만들기

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

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

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

���텐트를 구성할 때 파일의 이름 및 MIME 유형을 지정하고 EXTRA_INITIAL_URI 추가 인텐트를 사용하여 파일 선택 도구가 처음 로드될 때 표시해야 하는 파일이나 디렉터리의 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)
}

Java

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

Java

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

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

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

Kotlin

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

Java

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/ 디렉터리 및 모든 하위 디렉터리

선택된 위치에서 작업 실행

사용자가 시스템의 파일 선택 도구를 사용하여 파일이나 디렉터리를 선택하면 앱은 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.
        }
    }
}

Java

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

Java

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
}

Java

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

Java

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

Java

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)

Java

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);

동일한 미디어 URI 검색

getMediaUri() 메서드는 지정된 문서 제공자 URI와 동일한 미디어 저장소 URI를 제공합니다. 두 URI는 같은 기본 항목을 참조합니다. 미디어 저장소 URI를 사용하면 공유 저장소에서 미디어 파일에 더 쉽게 액세스할 수 있습니다.

getMediaUri() 메서드는 ExternalStorageProvider URI를 지원합니다. Android 12(API 수준 31) 이상에서는 이 메서드가 MediaDocumentsProvider 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
}

Java

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

Java

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

추가 리소스

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

샘플

동영상