page.title=저장소 액세스 프레임워크 @jd:body

이 문서의 내용 더 많은 결과 보기

  1. 개요
  2. 제어 흐름
  3. 클라이언트 앱 작성
    1. 문서 검색
    2. 결과 처리
    3. 문서 메타데이터 살펴보기
    4. 문서 열기
    5. 새 문서 생성하기
    6. 문서 삭제하기
    7. 문서 편집하기
    8. 권한 유지
  4. 사용자 지정 문서 제공자 작성하기
    1. 매니페스트
    2. 계약
    3. 하위 클래스 DocumentsProvider
    4. 보안

Key 클래스

  1. {@link android.provider.DocumentsProvider}
  2. {@link android.provider.DocumentsContract}

비디오

  1. DevBytes: Android 4.4 저장소 액세스 프레임워크: 제공자
  2. DevBytes: Android 4.4 저장소 액세스 프레임워크: 클라이언트

코드 샘플

  1. 저장소 제공자
  2. StorageClient

참고 항목

  1. 콘텐츠 제공자 기본 정보

Android 4.4(API 레벨 19)에서는 저장소 액세스 프레임워크(SAF)를 처음 도입하게 되었습니다. SAF는 사용자가 선호하는 문서 저장소 제공자 전체를 걸쳐 문서, 이미지 및 각종 다른 파일을 탐색하고 여는 작업을 간편하게 만들어줍니다. 표준형의, 사용하기 쉬운 UI로 사용자가 각종 앱과 제공자에 걸쳐 일관된 방식으로 파일을 탐색하고 최근 내용에 액세스할 수 있게 해줍니다.

클라우드 또는 로컬 저장소 서비스가 이 에코시스템에 참가하려면 자신의 서비스를 캡슐화하는 {@link android.provider.DocumentsProvider}를 구현하면 됩니다. 제공자의 문서에 액세스해야 하는 클라이언트 앱의 경우 단 몇 줄의 코드만으로 SAF와 통합할 수 있습니다.

SAF에는 다음과 같은 항목이 포함됩니다.

SAF가 제공하는 기능을 몇 가지 예로 들면 다음과 같습니다.

개요

SAF는 {@link android.provider.DocumentsProvider} 클래스의 하위 클래스인 콘텐츠 제공자를 중심으로 둘러싸고 있습니다. 데이터는 문서 제공자 내에서 일반적인 파일 계층으로 구조화됩니다.

data model

그림 1. 문서 제공자 데이터 모델입니다. 루트 하나가 하나의 문서를 가리키며, 이는 다시 트리 전체의 팬아웃을 시작합니다.

다음 내용을 참고하십시오.

제어 흐름

위에서 언급한 바와 같이, 문서 제공자 데이터 모델은 일반적인 파일 계층을 기반으로 합니다. 그러나, 데이터를 물리적으로 저장하는 방식은 마음대로 선택할 수 있습니다. 다만 {@link android.provider.DocumentsProvider} API를 통해 액세스할 수 있기만 하면 됩니다. 예를 들어, 데이터를 저장하기 위해 태그 기반 클라우드 저장소를 사용해도 됩니다.

그림 2는 사진 앱이 SAF를 사용하여 저장된 데이터에 액세스할 수 있는 방법을 예시로 나타낸 것입니다.

app

그림 2. 저장소 액세스 프레임워크 흐름

다음 내용을 참고하십시오.

그림 3은 이미지를 검색 중인 사용자가 Google Drive 계정을 선택한 선택기를 나타낸 것입니다.

picker

그림 3. 선택기

사용자가 Google Drive를 선택하면 이미지가 그림 4에 나타난 것처럼 표시됩니다. 그때부터 사용자는 제공자와 클라이언트 앱이 지원하는 방식이라면 어떤 식으로든 이들 이미지와 상호 작용할 수 있게 됩니다.

picker

그림 4. 이미지

클라이언트 앱 작성

Android 4.3 이하에서는 앱이 또 다른 앱에서 파일을 검색할 수 있도록 하려면 {@link android.content.Intent#ACTION_PICK} 또는 {@link android.content.Intent#ACTION_GET_CONTENT}와 같은 인텐트를 호출해야만 했습니다. 그런 다음 파일을 선택할 앱을 하나 선택하고, 선택한 앱이 사용자 인터페이스를 제공하여야 사용자가 이용 가능한 파일 중에서 탐색하고 선택할 수 있었습니다.

Android 4.4 이상에는 {@link android.content.Intent#ACTION_OPEN_DOCUMENT} 인텐트를 사용할 수 있다는 추가 옵션이 있습니다. 이는 시스템이 제어하는 선택기를 표시하여 사용자가 다른 앱에서 이용할 수 있게 만든 파일을 모두 탐색할 수 있게 해줍니다. 이 하나의 UI로부터 사용자는 지원되는 모든 앱에서 파일을 선택할 수 있는 것입니다.

{@link android.content.Intent#ACTION_OPEN_DOCUMENT}는 {@link android.content.Intent#ACTION_GET_CONTENT}를 대체할 목적으로 만들어진 것이 아닙니다. 어느 것을 사용해야 할지는 각자의 앱에 필요한 것이 무엇인지에 좌우됩니다.

이 섹션에서는 {@link android.content.Intent#ACTION_OPEN_DOCUMENT} 및 {@link android.content.Intent#ACTION_CREATE_DOCUMENT} 인텐트를 근거로 클라이언트 앱을 작성하는 방법을 설명합니다.

다음 조각에서는 {@link android.content.Intent#ACTION_OPEN_DOCUMENT}를 사용하여 이미지 파일이 들어 있는 문서 제공자를 검색합니다.

private static final int READ_REQUEST_CODE = 42;
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
public void performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones)
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only images, using the image MIME data type.
    // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
    // To search for all documents available via installed storage providers,
    // it would be "*/*".
    intent.setType("image/*");

    startActivityForResult(intent, READ_REQUEST_CODE);
}

다음 내용을 참고하십시오.

결과 처리

사용자가 선택기에서 문서를 선택하면 {@link android.app.Activity#onActivityResult onActivityResult()}가 호출됩니다. 선택한 문서를 가리키는 URI는 {@code resultData} 매개변수 안에 들어있습니다. 이 URI를 {@link android.content.Intent#getData getData()}를 사용하여 추출합니다. 일단 이것을 가지게 되면 이를 사용하여 사용자가 원하는 문서를 검색하면 됩니다. 예:

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {

    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
    // response to some other intent, and the code below shouldn't run at all.

    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        // The document selected by the user won't be returned in the intent.
        // Instead, a URI to that document will be contained in the return intent
        // provided to this method as a parameter.
        // Pull that URI using resultData.getData().
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            Log.i(TAG, "Uri: " + uri.toString());
            showImage(uri);
        }
    }
}

문서 메타데이터 살펴보기

문서의 URI를 얻은 다음에는 그 문서의 메타데이터에 액세스할 수 있습니다. 이 조각은 해당 URI가 나타내는 문서의 메타데이터를 가져와 다음과 같이 기록합니다.

public void dumpImageMetaData(Uri uri) {

    // The query, since it only applies to a single document, will only return
    // one row. There's no need to filter, sort, or select fields, since 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 since an
            // int can't be null in Java, the behavior is implementation-specific,
            // which is just a fancy term for "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를 얻은 다음에는 문서를 열 수도 있고 원하는 대로 무엇이든 할 수 있습니다.

비트맵

다음은 {@link android.graphics.Bitmap}을 여는 방법을 예시로 나타낸 것입니다.

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

이 작업을 UI 스레드에서 해서는 안 된다는 점을 유의하십시오. 이것은 배경에서 하되, {@link android.os.AsyncTask}를 사용합니다. 비트맵을 열고 나면 이를 {@link android.widget.ImageView}로 표시할 수 있습니다.

InputStream 가져오기

다음은 URI에서 {@link java.io.InputStream}을 가져오는 방법을 예시로 나타낸 것입니다. 이 조각에서 파일의 줄이 문자열로 읽히고 있습니다.

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

새 문서 생성하기

개발자의 앱은 문서 제공자에서 새 문서를 생성할 수 있습니다. 이때 {@link android.content.Intent#ACTION_CREATE_DOCUMENT} 인텐트를 사용하면 됩니다. 파일을 생성하려면 인텐트에 MIME 유형과 파일 이름을 부여하고, 고유한 요청 코드로 이를 시작하면 됩니다. 나머지는 여러분 대신 알아서 해드립니다.

// Here are some examples of how you might call this method.
// The first parameter is the MIME type, and the second parameter is the name
// of the file you are creating:
//
// createFile("text/plain", "foobar.txt");
// createFile("image/png", "mypicture.png");

// Unique request code.
private static final int WRITE_REQUEST_CODE = 43;
...
private void createFile(String mimeType, String fileName) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

    // Filter to only show results that can be "opened", such as
    // a file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Create a file with the requested MIME type.
    intent.setType(mimeType);
    intent.putExtra(Intent.EXTRA_TITLE, fileName);
    startActivityForResult(intent, WRITE_REQUEST_CODE);
}

새 문서를 생성하고 나면 {@link android.app.Activity#onActivityResult onActivityResult()}에서 URI를 가져와 거기에 계속해서 쓸 수 있습니다.

문서 삭제하기

어느 문서에 대한 URI가 있고 해당 문서의 {@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS} 에 {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE}가 들어 있는 경우, 해당 문서를 삭제할 수 있습니다. 예:

DocumentsContract.deleteDocument(getContentResolver(), uri);

문서 편집하기

준비된 텍스트 문서를 편집하는 데 SAF를 사용할 수 있습니다. 이 조각은 {@link android.content.Intent#ACTION_OPEN_DOCUMENT} 인텐트를 실행하며 {@link android.content.Intent#CATEGORY_OPENABLE} 카테고리를 사용해 열 수 있는 문서만 표시하도록 합니다. 이것을 한층 더 필터링하여 텍스트 파일만 표시하게 하려면 다음과 같이 합니다.

private static final int EDIT_REQUEST_CODE = 44;
/**
 * Open a file for writing and append some text to it.
 */
 private void editDocument() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
    // file browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only text files.
    intent.setType("text/plain");

    startActivityForResult(intent, EDIT_REQUEST_CODE);
}

다음으로, {@link android.app.Activity#onActivityResult onActivityResult()} (결과 처리 참조)에서 코드를 호출하여 편집 작업을 수행하도록 하면 됩니다. 다음 조각은 {@link android.content.ContentResolver}에서 {@link java.io.FileOutputStream} 을 가져온 것입니다. 이것은 기본적으로 "쓰기" 모드를 사용합니다. 필요한 최소 수량의 액세스만을 요청하는 것이 가장 좋으니 쓰기만 필요하다면 읽기/쓰기를 요청하지 마십시오.

private void alterDocument(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten by MyCloud 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 권한 허가를 부여합니다. 이것은 사용자의 장치를 다시 시작할 때까지 유지됩니다. 하지만 만일 앱이 이미지 편집 앱이고, 사용자가 여러분의 앱에서 바로 편집한 5개의 이미지에 액세스할 수 있도록 하고자 한다고 가정해봅시다. 사용자의 기기가 재시작되면 여러분이 사용자에게 시스템 선택기를 다시 보내 해당 파일을 검색하도록 해야 할 텐데, 이것은 물론 이상적인 것과는 거리가 멉니다.

이런 일이 일어나지 않도록 방지하기 위해 시스템이 앱에 부여한 권한을 유지할 수 있습니다. 여러분의 앱은 시스템이 제공하는 유지 가능한 URI 권한 허가를 효율적으로 "받아들입니다". 이렇게 하면 사용자가 여러분의 앱을 통해 파일에 지속적인 액세스 권한을 가질 수 있으며, 이는 기기가 다시 시작되더라도 관계 없습니다.

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를 저장해두었을 수 있지만, 이는 더 이상 유효하지 않을 수 있습니다. 또 다른 앱이 문서를 삭제하거나 수정했을 수 있기 때문입니다. 따라서, 항상 {@code getContentResolver().takePersistableUriPermission()}을 호출하여 최신 데이터를 확인해야 합니다.

사용자 지정 문서 제공자 작성하기

파일용 저장소 서비스를 제공하는 앱을 개발 중인 경우(예를 들어 클라우드 저장 서비스 등), SAF를 통해 파일을 사용할 수 있도록 하려면 사용자 지정 문서 제공자를 작성하면 됩니다. 이 섹션에서는 이렇게 하는 방법을 설명합니다.

매니페스트

사용자 지정 문서 제공자를 구현하려면 애플리케이션의 매니페스트에 다음과 같은 항목을 추가하십시오.

다음은 제공자를 포함한 샘플 매니페스트에서 발췌한 것입니다.

<manifest... >
    ...
    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />
        ....
        <provider
            android:name="com.example.android.storageprovider.MyCloudProvider"
            android:authorities="com.example.android.storageprovider.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS"
            android:enabled="@bool/atLeastKitKat">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

Android 4.3 이하에서 실행되는 기기 지원

{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 인텐트는 Android 4.4 이상에서 실행되는 기기에서만 사용할 수 있습니다. 애플리케이션이 {@link android.content.Intent#ACTION_GET_CONTENT}를 지원하도록 하여 Android 4.3 이하에서 실행되는 기기에도 적용되도록 하려면 Android 4.4 이상에서 실행되는 기기용 매니페스트에 있는 {@link android.content.Intent#ACTION_GET_CONTENT} 인텐트 필터를 비활성화해야 합니다. 문서 제공자와 {@link android.content.Intent#ACTION_GET_CONTENT}는 상호 배타적인 것으로 간주해야 합니다. 둘을 모두 동시에 지원하는 경우, 앱이 시스템 선택기 UI에 두 번 나타나 저장된 데이터에 액세스할 두 가지 서로 다른 방법을 제안하게 됩니다. 이렇게 되면 사용자에게 혼동을 주게 되겠죠.

다음은 Android 버전 4.4 이상에서 실행되는 기기용 {@link android.content.Intent#ACTION_GET_CONTENT} 인텐트 필터를 비활성화하는 데 권장되는 방법입니다.

  1. {@code res/values/} 아래의 {@code bool.xml} 리소스 파일에 이 라인을 추가합니다.
    <bool name="atMostJellyBeanMR2">true</bool>
  2. {@code res/values-v19/} 아래의 {@code bool.xml} 리소스 파일에 이 라인을 추가합니다.
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 액티비티 별칭을 추가하여 버전 4.4(API 레벨 19) 이상을 대상으로 한 {@link android.content.Intent#ACTION_GET_CONTENT} 인텐트 필터를 비활성화합니다. 예:
    <!-- This activity alias is added so that GET_CONTENT intent-filter
         can be disabled for builds on API level 19 and higher. -->
    <activity-alias android:name="com.android.example.app.MyPicker"
            android:targetActivity="com.android.example.app.MyActivity"
            ...
            android:enabled="@bool/atMostJellyBeanMR2">
        <intent-filter>
            <action android:name="android.intent.action.GET_CONTENT" />
            <category android:name="android.intent.category.OPENABLE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="image/*" />
            <data android:mimeType="video/*" />
        </intent-filter>
    </activity-alias>
    

계약

사용자 지정 제공자를 작성할 때면 일반적으로 수반되는 작업 중 하나가 계약 클래스를 구현하는 것입니다. 이는 콘텐츠 제공자 개발자 가이드에서 설명한 것과 같습니다. 계약 클래스는 {@code public final} 클래스로, 이 안에 URI에 대한 상수 정의, 열 이름, MIME 유형 및 제공자에 관련된 다른 메타 데이터가 들어 있습니다. SAF가 이와 같은 계약 클래스를 대신 제공해주므로 직접 쓰지 않아도 됩니다.

예를 들어 다음은 여러분의 문서 제공자가 문서 또는 루트에 대해 쿼리된 경우 커서로 반환할 수 있는 열을 나타낸 것입니다.

private static final String[] DEFAULT_ROOT_PROJECTION =
        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
        Root.COLUMN_AVAILABLE_BYTES,};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};

하위 클래스 DocumentsProvider

사용자 지정 문서 제공자를 작성하기 위한 다음 단계는 추상 클래스 {@link android.provider.DocumentsProvider}를 하위 클래스로 만드는 것입니다. 최소한 다음과 같은 메서드를 구현해야 합니다.

꼭 구현해야만 하는 메서드는 이들뿐이지만, 개발자 여러분이 구현하고자 하는 메서드는 이보다 훨씬 많을 수도 있습니다. 자세한 내용은{@link android.provider.DocumentsProvider} 를 참조하십시오.

QueryRoots 구현

{@link android.provider.DocumentsProvider#queryRoots queryRoots()} 구현은 반드시 {@link android.database.Cursor}를 반환해야 하며, 이는 문서 제공자의 모든 루트 디렉터리를 가리켜야 합니다. 이때 {@link android.provider.DocumentsContract.Root}에서 정의한 열을 사용합니다.

다음 조각에서 {@code projection} 매개변수는 발신자가 돌려받고자 하는 특정 필드를 나타냅니다. 이 조각은 새 커서를 생성하며 그에 하나의 행을 추가합니다. 하나의 루트, 다운로드 또는 이미지와 같은 최상위 레벨 디렉터리가 해당됩니다. 대부분의 제공자에는 루트가 하나뿐입니다. 하나 이상이 있을 수도 있습니다. 예를 들어 사용자 계정이 여러 개인 경우가 있습니다. 그런 경우에는 커서에 두 번째 행을 추가하면 됩니다.

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Create a cursor with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    // Construct one row for a root called "MyCloud".
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change once it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));

    // The child MIME types are used to filter the roots and only present to the
    //  user roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

QueryChildDocuments 구현

{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()} 구현은 반드시 {@link android.database.Cursor}를 반환해야 하며, 이는 지정된 디렉터리 내의 모든 파일을 가리켜야 합니다. 이때 {@link android.provider.DocumentsContract.Document}에서 정의한 열을 사용합니다.

이 메서드는 선택기 UI에서 애플리케이션 루트를 선택하는 경우 호출됩니다. 이는 해당 루트 아래 디렉터리의 하위 문서를 가져옵니다. 이것은 루트에서뿐만 아니라 파일 계층의 어느 레벨에서나 호출할 수 있습니다. 이 조각은 요청한 열로 새 커서를 만든 다음, 상위 디렉터리에 있는 모든 직속 하위에 대한 정보를 커서에 추가합니다. 하위는 이미지, 또 다른 디렉터리가 될 수도 있고 어느 파일이라도 될 수 있습니다.

@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
                              String sortOrder) throws FileNotFoundException {

    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    final File parent = getFileForDocId(parentDocumentId);
    for (File file : parent.listFiles()) {
        // Adds the file's display name, MIME type, size, and so on.
        includeFile(result, null, file);
    }
    return result;
}

QueryDocuments 구현

{@link android.provider.DocumentsProvider#queryDocument queryDocument()} 구현은 반드시 {@link android.database.Cursor}를 반환해야 하며, 이는 지정된 파일을 가리켜야 합니다. 이때 {@link android.provider.DocumentsContract.Document}에서 정의한 열을 사용합니다.

{@link android.provider.DocumentsProvider#queryDocument queryDocument()} 메서드는 {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}에서 전달된 것과 같은 정보를 반환하지만, 특정한 파일에만 해당됩니다.

@Override
public Cursor queryDocument(String documentId, String[] projection) throws
        FileNotFoundException {

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    includeFile(result, documentId, null);
    return result;
}

OpenDocument 구현

지정된 파일을 나타내는 {@link android.os.ParcelFileDescriptor}를 반환하려면 {@link android.provider.DocumentsProvider#openDocument openDocument()}를 구현해야 합니다. 다른 앱들이 반환된 {@link android.os.ParcelFileDescriptor}를 사용하여 데이터를 스트리밍할 수 있습니다. 시스템은 사용자가 파일을 선택하고 클라이언트 앱이 이에 대한 액세스를 요청하면서 {@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}를 사용할 때 이 메서드를 호출합니다. 예를 들면 다음과 같습니다.

@Override
public ParcelFileDescriptor openDocument(final String documentId,
                                         final String mode,
                                         CancellationSignal signal) throws
        FileNotFoundException {
    Log.v(TAG, "openDocument, mode: " + mode);
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    final File file = getFileForDocId(documentId);

    final boolean isWrite = (mode.indexOf('w') != -1);
    if(isWrite) {
        // Attach a close listener if the document is opened in write mode.
        try {
            Handler handler = new Handler(getContext().getMainLooper());
            return ParcelFileDescriptor.open(file, accessMode, handler,
                        new ParcelFileDescriptor.OnCloseListener() {
                @Override
                public void onClose(IOException e) {

                    // Update the file with the cloud server. The client is done
                    // writing.
                    Log.i(TAG, "A file with id " +
                    documentId + " has been closed!
                    Time to " +
                    "update the server.");
                }

            });
        } catch (IOException e) {
            throw new FileNotFoundException("Failed to open document with id "
            + documentId + " and mode " + mode);
        }
    } else {
        return ParcelFileDescriptor.open(file, accessMode);
    }
}

보안

여러분의 문서 제공자가 암호로 보호된 클라우드 저장소 서비스이고 여러분은 사용자가 파일을 공유하기 전에 우선 로그인부터 하도록 확실히 해두고 싶다고 가정합니다. 사용자가 로그인되지 않은 경우 앱은 어떻게 해야 합니까? 해법은 {@link android.provider.DocumentsProvider#queryRoots queryRoots()} 구현에서 루트를 반환하지 않는 것입니다. 다시 말해, 텅 빈 루트 커서를 반환하는 것입니다.

public Cursor queryRoots(String[] projection) throws FileNotFoundException {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
}

또 다른 단계는 {@code getContentResolver().notifyChange()}를 호출하는 것입니다. {@link android.provider.DocumentsContract}를 기억하십니까? 이것을 사용하는 이유는 바로 이 URI를 만들기 위해서입니다. 다음 조각은 사용자의 로그인 상태가 변경될 때마다 시스템이 문서 제공자의 루트를 쿼리하도록 지시하고 있습니다. 사용자가 로그인되어 있지 않은 상태에서 {@link android.provider.DocumentsProvider#queryRoots queryRoots()}를 호출하면 위에서 나타낸 것과 같이 빈 커서를 반환합니다. 이렇게 하면 사용자가 제공자에 로그인되었을 때만 제공자의 문서를 사용할 수 있도록 보장할 수 있습니다.

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}