• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1page.title=저장소 액세스 프레임워크
2@jd:body
3<div id="qv-wrapper">
4<div id="qv">
5
6<h2>이 문서의 내용
7 <a href="#" onclick="hideNestedItems('#toc44',this);return false;" class="header-toggle">
8        <span class="more">더 많은 결과 보기</span>
9        <span class="less" style="display:none">결과 숨기기</span></a></h2>
10<ol id="toc44" class="hide-nested">
11    <li>
12        <a href="#overview">개요</a>
13    </li>
14    <li>
15        <a href="#flow">제어 흐름</a>
16    </li>
17    <li>
18        <a href="#client">클라이언트 앱 작성</a>
19        <ol>
20        <li><a href="#search">문서 검색</a></li>
21        <li><a href="#process">결과 처리</a></li>
22        <li><a href="#metadata">문서 메타데이터 살펴보기</a></li>
23        <li><a href="#open">문서 열기</a></li>
24        <li><a href="#create">새 문서 생성하기</a></li>
25        <li><a href="#delete">문서 삭제하기</a></li>
26        <li><a href="#edit">문서 편집하기</a></li>
27        <li><a href="#permissions">권한 유지</a></li>
28        </ol>
29    </li>
30    <li><a href="#custom">사용자 지정 문서 제공자 작성하기</a>
31        <ol>
32        <li><a href="#manifest">매니페스트</a></li>
33        <li><a href="#contract">계약</a></li>
34        <li><a href="#subclass">하위 클래스 DocumentsProvider</a></li>
35        <li><a href="#security">보안</a></li>
36        </ol>
37    </li>
38
39</ol>
40<h2>Key 클래스</h2>
41<ol>
42    <li>{@link android.provider.DocumentsProvider}</li>
43    <li>{@link android.provider.DocumentsContract}</li>
44</ol>
45
46<h2>비디오</h2>
47
48<ol>
49    <li><a href="http://www.youtube.com/watch?v=zxHVeXbK1P4">
50DevBytes: Android 4.4 저장소 액세스 프레임워크: 제공자</a></li>
51     <li><a href="http://www.youtube.com/watch?v=UFj9AEz0DHQ">
52DevBytes: Android 4.4 저장소 액세스 프레임워크: 클라이언트</a></li>
53</ol>
54
55
56<h2>코드 샘플</h2>
57
58<ol>
59    <li><a href="{@docRoot}samples/StorageProvider/index.html">
60저장소 제공자</a></li>
61     <li><a href="{@docRoot}samples/StorageClient/index.html">
62StorageClient</a></li>
63</ol>
64
65<h2>참고 항목</h2>
66<ol>
67    <li>
68        <a href="{@docRoot}guide/topics/providers/content-provider-basics.html">
69        콘텐츠 제공자 기본 정보
70        </a>
71    </li>
72</ol>
73
74</div>
75</div>
76
77
78<p>Android 4.4(API 레벨 19)에서는 저장소 액세스 프레임워크(SAF)를 처음 도입하게 되었습니다. SAF는
79사용자가 선호하는 문서 저장소 제공자 전체를 걸쳐 문서, 이미지 및 각종 다른 파일을
80탐색하고 여는 작업을 간편하게 만들어줍니다. 표준형의, 사용하기 쉬운 UI로
81사용자가 각종 앱과 제공자에 걸쳐 일관된 방식으로 파일을 탐색하고 최근 내용에 액세스할 수 있게 해줍니다.</p>
82
83<p>클라우드 또는 로컬 저장소 서비스가 이 에코시스템에 참가하려면 자신의 서비스를 캡슐화하는
84{@link android.provider.DocumentsProvider}를 구현하면 됩니다.
85제공자의 문서에 액세스해야 하는 클라이언트 앱의 경우 단 몇 줄의 코드만으로
86SAF와 통합할 수 있습니다.</p>
87
88<p>SAF에는 다음과 같은 항목이 포함됩니다.</p>
89
90<ul>
91<li><strong>문서 제공자</strong>&mdash;일종의 콘텐츠 제공자로
92저장소 서비스(예: Google Drive 등)로 하여금 자신이 관리하는 파일을 드러내도록 허용합니다. 문서 제공자는
93{@link android.provider.DocumentsProvider} 클래스의 하위 클래스로 구현됩니다.
94문서 제공자 스키마는 기존의 파일 계층을 근거로 하지만,
95문서 제공자가 데이터를 저장하는 물리적인 방법은 개발자가 선택하기 나름입니다.
96Android 플랫폼에는 내장된 문서 제공자가 여러 개 있습니다.
97예를 들어 다운로드, 이미지 및 비디오 등입니다.</li>
98
99<li><strong>클라이언트 앱</strong>&mdash;일종의 사용자 지정 앱으로
100{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 및/또는
101{@link android.content.Intent#ACTION_CREATE_DOCUMENT} 인텐트를 호출하고,
102문서 제공자가 반환하는 파일을 수신합니다.</li>
103
104<li><strong>선택기</strong>&mdash;일종의 시스템 UI로 사용자가 클라이언트 앱의
105검색 기준을 만족하는 모든 문서 제공자에서 문서에 액세스할 수 있도록 해줍니다.</li>
106</ul>
107
108<p>SAF가 제공하는 기능을 몇 가지 예로 들면 다음과 같습니다.</p>
109<ul>
110<li>사용자들로 하여금 하나의 앱만이 아니라 모든 문서 제공자에서 콘텐츠를 탐색할 수 있게 해줍니다.</li>
111<li>여러분의 앱이 문서 제공자가 소유한 문서에 대한 장기적, 영구적 액세스 권한을 가질 수 있도록
112해줍니다. 이 액세스 권한을 통해 사용자가 제공자에 있는 파일을 추가, 편집,
113저장 및 삭제할 수 있습니다.</li>
114<li>여러 개의 사용자 계정을 지원하며 USB 저장소 제공자와 같은 임시 루트도 지원합니다.
115이는 드라이브가 연결되어 있을 때만 나타납니다. </li>
116</ul>
117
118<h2 id ="overview">개요</h2>
119
120<p>SAF는 {@link android.provider.DocumentsProvider} 클래스의
121하위 클래스인 콘텐츠 제공자를 중심으로 둘러싸고 있습니다. 데이터는 <em>문서 제공자</em> 내에서 일반적인 파일 계층으로
122구조화됩니다.</p>
123<p><img src="{@docRoot}images/providers/storage_datamodel.png" alt="data model" /></p>
124<p class="img-caption"><strong>그림 1.</strong> 문서 제공자 데이터 모델입니다. 루트 하나가 하나의 문서를 가리키며,
125이는 다시 트리 전체의 팬아웃을 시작합니다.</p>
126
127<p>다음 내용을 참고하십시오.</p>
128<ul>
129
130<li>각 문서 제공자는 하나 이상의 "루트"를 보고합니다.
131이는 문서 트리 속을 탐색할 시작 지점입니다.
132각 루트에는 고유한 {@link android.provider.DocumentsContract.Root#COLUMN_ROOT_ID}가 있으며,
133이는 해당 루트 아래의 콘텐츠를 나타내는 문서(디렉터리)를
134가리킵니다.
135루트는 설계상 동적으로 만들어져 있어 여러 개의 계정, 임시 USB 저장소 기기
136또는 사용자 로그인/로그아웃 등과 같은 경우를 지원하도록 되어 있습니다.</li>
137
138<li>각 루트 아래에 문서가 하나씩 있습니다. 해당 문서는 1부터 <em>N</em>까지의 문서를 가리키는데,
139이는 각각 1부터 <em>N</em>의 문서를 가리킬 수 있습니다. </li>
140
141<li>각 저장소의 백엔드가
142개별적인 파일과 디렉터리를 고유한
143{@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID}로
144참조하여 드러냅니다.문서 ID는 고유해야 하며 한 번 발행되고 나면 변경되지 않습니다.
145이들은 기기 재부팅을 통괄하여 영구적인 URI 허가에 사용되기 때문입니다.</li>
146
147
148<li>문서는 열 수 있는 파일이거나(특정 MIME 유형으로),
149추가 문서가 들어있는 디렉터리일 수 있습니다(
150{@link android.provider.DocumentsContract.Document#MIME_TYPE_DIR} MIME 유형으로).</li>
151
152<li>각 문서는 서로 다른 기능을 가지고 있을 수 있습니다. 이는
153{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS COLUMN_FLAGS}에서 설명한 것과 같습니다.
154 예를 들어 {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE},
155{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE} 및
156{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_THUMBNAIL} 등입니다.
157같은 {@link android.provider.DocumentsContract.Document#COLUMN_DOCUMENT_ID}가
158여러 디렉터리에 포함되어 있을 수도 있습니다.</li>
159</ul>
160
161<h2 id="flow">제어 흐름</h2>
162<p>위에서 언급한 바와 같이, 문서 제공자 데이터 모델은 일반적인
163파일 계층을 기반으로 합니다. 그러나, 데이터를 물리적으로 저장하는 방식은 마음대로 선택할 수 있습니다. 다만
164{@link android.provider.DocumentsProvider} API를 통해 액세스할 수 있기만 하면 됩니다.
165예를 들어, 데이터를 저장하기 위해 태그 기반 클라우드 저장소를 사용해도 됩니다.</p>
166
167<p>그림 2는 사진 앱이 SAF를 사용하여 저장된 데이터에 액세스할 수 있는 방법을
168예시로 나타낸 것입니다.</p>
169<p><img src="{@docRoot}images/providers/storage_dataflow.png" alt="app" /></p>
170
171<p class="img-caption"><strong>그림 2.</strong> 저장소 액세스 프레임워크 흐름</p>
172
173<p>다음 내용을 참고하십시오.</p>
174<ul>
175
176<li>SAF에서는 제공자와 클라이언트가 직접 상호 작용하지 않습니다.
177 클라이언트가 파일과 상호 작용하기 위한 권한을 요청합니다(다시 말해,
178파일을 읽고, 편집하고 생성 또는 삭제할 권한을 말합니다).</li>
179
180<li>상호 작용은 애플리케이션(이 예시에서는 주어진 사진 앱)이 인텐트
181{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 또는 {@link android.content.Intent#ACTION_CREATE_DOCUMENT}를 실행시키면 시작합니다. 이 인텐트에는
182기준을 한층 더 정밀하게 하기 위한 필터가 포함될 수 있습니다. 예를 들어, "열 수 있는 파일 중에서
183'이미지' MIME 유형을 가진 파일을 모두 주세요"라고 할 수 있습니다.</li>
184
185<li>인텐트가 실행되면 시스템 선택기가 각각의 등록된 제공자로 이동하여 사용자에게
186일치하는 콘텐츠 루트를 보여줍니다.</li>
187
188<li>선택기는 사용자에게 문서에 액세스하는 데 쓰는 표준 인터페이스를 부여합니다. 이는
189기본 문서 제공자 사이에 큰 차이가 있더라도 무관합니다. 예를 들어, 그림 2는
190Google Drive 제공자, USB 제공자와 클라우드 제공자를 나타낸 것입니다.</li>
191</ul>
192
193<p>그림 3은 이미지를 검색 중인 사용자가 Google Drive 계정을 선택한
194선택기를 나타낸 것입니다.</p>
195
196<p><img src="{@docRoot}images/providers/storage_picker.png" width="340" alt="picker" style="border:2px solid #ddd" /></p>
197
198<p class="img-caption"><strong>그림 3.</strong> 선택기</p>
199
200<p>사용자가 Google Drive를 선택하면 이미지가 그림 4에 나타난 것처럼
201표시됩니다. 그때부터 사용자는 제공자와 클라이언트 앱이 지원하는 방식이라면 어떤 식으로든
202이들 이미지와 상호 작용할 수 있게 됩니다.
203
204<p><img src="{@docRoot}images/providers/storage_photos.png" width="340" alt="picker" style="border:2px solid #ddd" /></p>
205
206<p class="img-caption"><strong>그림 4.</strong> 이미지</p>
207
208<h2 id="client">클라이언트 앱 작성</h2>
209
210<p>Android 4.3 이하에서는 앱이 또 다른 앱에서 파일을 검색할 수 있도록 하려면
211 {@link android.content.Intent#ACTION_PICK}
212 또는 {@link android.content.Intent#ACTION_GET_CONTENT}와 같은 인텐트를 호출해야만 했습니다. 그런 다음
213파일을 선택할 앱을 하나 선택하고, 선택한 앱이 사용자 인터페이스를 제공하여야 사용자가
214이용 가능한 파일 중에서 탐색하고 선택할 수 있었습니다. </p>
215
216<p>Android 4.4 이상에는
217{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 인텐트를 사용할 수 있다는 추가 옵션이 있습니다.
218이는 시스템이 제어하는 선택기를 표시하여 사용자가 다른 앱에서 이용할 수 있게 만든 파일을
219모두 탐색할 수 있게 해줍니다. 이 하나의 UI로부터
220사용자는 지원되는 모든 앱에서 파일을 선택할 수 있는 것입니다.</p>
221
222<p>{@link android.content.Intent#ACTION_OPEN_DOCUMENT}는
223{@link android.content.Intent#ACTION_GET_CONTENT}를
224대체할 목적으로 만들어진 것이 아닙니다. 어느 것을 사용해야 할지는 각자의 앱에 필요한 것이 무엇인지에 좌우됩니다.</p>
225
226<ul>
227<li>앱이 단순히 데이터를 읽고/가져오기만을 바란다면
228{@link android.content.Intent#ACTION_GET_CONTENT}를 사용하십시오.
229이 방식을 사용하면 앱은 이미지 파일과 같은 데이터 사본을 가져오게 됩니다.</li>
230
231<li>앱이 문서 제공자가 보유한 문서에 장기적, 영구적 액세스 권한을 가지기를 바라는 경우
232{@link android.content.Intent#ACTION_OPEN_DOCUMENT}를 사용하십시오.
233 일례로 사용자들에게 문서 제공자에 저장된 이미지를 편집할 수 있게 해주는
234사진 편집 앱을 들 수 있겠습니다. </li>
235
236</ul>
237
238
239<p>이 섹션에서는
240{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 및
241{@link android.content.Intent#ACTION_CREATE_DOCUMENT} 인텐트를 근거로 클라이언트 앱을 작성하는 방법을 설명합니다.</p>
242
243
244<h3 id="search">문서 검색</h3>
245
246<p>
247다음 조각에서는 {@link android.content.Intent#ACTION_OPEN_DOCUMENT}를
248사용하여 이미지 파일이 들어 있는 문서 제공자를
249검색합니다.</p>
250
251<pre>private static final int READ_REQUEST_CODE = 42;
252...
253/**
254 * Fires an intent to spin up the &quot;file chooser&quot; UI and select an image.
255 */
256public void performFileSearch() {
257
258    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
259    // browser.
260    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
261
262    // Filter to only show results that can be &quot;opened&quot;, such as a
263    // file (as opposed to a list of contacts or timezones)
264    intent.addCategory(Intent.CATEGORY_OPENABLE);
265
266    // Filter to show only images, using the image MIME data type.
267    // If one wanted to search for ogg vorbis files, the type would be &quot;audio/ogg&quot;.
268    // To search for all documents available via installed storage providers,
269    // it would be &quot;*/*&quot;.
270    intent.setType(&quot;image/*&quot;);
271
272    startActivityForResult(intent, READ_REQUEST_CODE);
273}</pre>
274
275<p>다음 내용을 참고하십시오.</p>
276<ul>
277<li>앱이 {@link android.content.Intent#ACTION_OPEN_DOCUMENT}
278 인텐트를 실행시키면 이는 일치하는 문서 제공자를 모두 표시하는 선택기를 시작합니다.</li>
279
280<li>{@link android.content.Intent#CATEGORY_OPENABLE} 카테고리를
281인텐트에 추가하면 결과를 필터링하여 이미지 파일 등 열 수 있는 문서만 표시합니다.</li>
282
283<li>{@code intent.setType("image/*")} 문으로 한층 더 필터링을 수행하여
284MIME 데이터 유형이 이미지인 문서만 표시하도록 합니다.</li>
285</ul>
286
287<h3 id="results">결과 처리</h3>
288
289<p>사용자가 선택기에서 문서를 선택하면
290{@link android.app.Activity#onActivityResult onActivityResult()}가 호출됩니다.
291선택한 문서를 가리키는 URI는 {@code resultData}
292매개변수 안에 들어있습니다. 이 URI를 {@link android.content.Intent#getData getData()}를 사용하여 추출합니다.
293일단 이것을 가지게 되면 이를 사용하여 사용자가 원하는 문서를 검색하면 됩니다. 예:
294</p>
295
296<pre>&#64;Override
297public void onActivityResult(int requestCode, int resultCode,
298        Intent resultData) {
299
300    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
301    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
302    // response to some other intent, and the code below shouldn't run at all.
303
304    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
305        // The document selected by the user won't be returned in the intent.
306        // Instead, a URI to that document will be contained in the return intent
307        // provided to this method as a parameter.
308        // Pull that URI using resultData.getData().
309        Uri uri = null;
310        if (resultData != null) {
311            uri = resultData.getData();
312            Log.i(TAG, "Uri: " + uri.toString());
313            showImage(uri);
314        }
315    }
316}
317</pre>
318
319<h3 id="metadata">문서 메타데이터 살펴보기</h3>
320
321<p>문서의 URI를 얻은 다음에는 그 문서의 메타데이터에 액세스할 수 있습니다. 이
322조각은 해당 URI가 나타내는 문서의 메타데이터를 가져와 다음과 같이 기록합니다.</p>
323
324<pre>public void dumpImageMetaData(Uri uri) {
325
326    // The query, since it only applies to a single document, will only return
327    // one row. There's no need to filter, sort, or select fields, since we want
328    // all fields for one document.
329    Cursor cursor = getActivity().getContentResolver()
330            .query(uri, null, null, null, null, null);
331
332    try {
333    // moveToFirst() returns false if the cursor has 0 rows.  Very handy for
334    // &quot;if there's anything to look at, look at it&quot; conditionals.
335        if (cursor != null &amp;&amp; cursor.moveToFirst()) {
336
337            // Note it's called &quot;Display Name&quot;.  This is
338            // provider-specific, and might not necessarily be the file name.
339            String displayName = cursor.getString(
340                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
341            Log.i(TAG, &quot;Display Name: &quot; + displayName);
342
343            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
344            // If the size is unknown, the value stored is null.  But since an
345            // int can't be null in Java, the behavior is implementation-specific,
346            // which is just a fancy term for &quot;unpredictable&quot;.  So as
347            // a rule, check if it's null before assigning to an int.  This will
348            // happen often:  The storage API allows for remote files, whose
349            // size might not be locally known.
350            String size = null;
351            if (!cursor.isNull(sizeIndex)) {
352                // Technically the column stores an int, but cursor.getString()
353                // will do the conversion automatically.
354                size = cursor.getString(sizeIndex);
355            } else {
356                size = &quot;Unknown&quot;;
357            }
358            Log.i(TAG, &quot;Size: &quot; + size);
359        }
360    } finally {
361        cursor.close();
362    }
363}
364</pre>
365
366<h3 id="open-client">문서 열기</h3>
367
368<p>문서의 URI를 얻은 다음에는 문서를 열 수도 있고 원하는 대로 무엇이든
369할 수 있습니다.</p>
370
371<h4>비트맵</h4>
372
373<p>다음은 {@link android.graphics.Bitmap}을 여는 방법을 예시로 나타낸 것입니다.</p>
374
375<pre>private Bitmap getBitmapFromUri(Uri uri) throws IOException {
376    ParcelFileDescriptor parcelFileDescriptor =
377            getContentResolver().openFileDescriptor(uri, "r");
378    FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
379    Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
380    parcelFileDescriptor.close();
381    return image;
382}
383</pre>
384
385<p>이 작업을 UI 스레드에서 해서는 안 된다는 점을 유의하십시오. 이것은 배경에서 하되,
386{@link android.os.AsyncTask}를 사용합니다. 비트맵을 열고 나면 이를
387{@link android.widget.ImageView}로 표시할 수 있습니다.
388</p>
389
390<h4>InputStream 가져오기</h4>
391
392<p>다음은 URI에서 {@link java.io.InputStream}을 가져오는 방법을 예시로 나타낸 것입니다. 이 조각에서
393파일의 줄이 문자열로 읽히고 있습니다.</p>
394
395<pre>private String readTextFromUri(Uri uri) throws IOException {
396    InputStream inputStream = getContentResolver().openInputStream(uri);
397    BufferedReader reader = new BufferedReader(new InputStreamReader(
398            inputStream));
399    StringBuilder stringBuilder = new StringBuilder();
400    String line;
401    while ((line = reader.readLine()) != null) {
402        stringBuilder.append(line);
403    }
404    fileInputStream.close();
405    parcelFileDescriptor.close();
406    return stringBuilder.toString();
407}
408</pre>
409
410<h3 id="create">새 문서 생성하기</h3>
411
412<p>개발자의 앱은 문서 제공자에서 새 문서를 생성할 수 있습니다. 이때
413{@link android.content.Intent#ACTION_CREATE_DOCUMENT}
414 인텐트를 사용하면 됩니다. 파일을 생성하려면 인텐트에 MIME 유형과 파일 이름을 부여하고,
415고유한 요청 코드로 이를 시작하면 됩니다. 나머지는 여러분 대신 알아서 해드립니다.</p>
416
417
418<pre>
419// Here are some examples of how you might call this method.
420// The first parameter is the MIME type, and the second parameter is the name
421// of the file you are creating:
422//
423// createFile("text/plain", "foobar.txt");
424// createFile("image/png", "mypicture.png");
425
426// Unique request code.
427private static final int WRITE_REQUEST_CODE = 43;
428...
429private void createFile(String mimeType, String fileName) {
430    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
431
432    // Filter to only show results that can be &quot;opened&quot;, such as
433    // a file (as opposed to a list of contacts or timezones).
434    intent.addCategory(Intent.CATEGORY_OPENABLE);
435
436    // Create a file with the requested MIME type.
437    intent.setType(mimeType);
438    intent.putExtra(Intent.EXTRA_TITLE, fileName);
439    startActivityForResult(intent, WRITE_REQUEST_CODE);
440}
441</pre>
442
443<p>새 문서를 생성하고 나면
444{@link android.app.Activity#onActivityResult onActivityResult()}에서 URI를 가져와 거기에 계속해서
445쓸 수 있습니다.</p>
446
447<h3 id="delete">문서 삭제하기</h3>
448
449<p>어느 문서에 대한 URI가 있고 해당 문서의
450{@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS}
451452{@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE}가 들어 있는 경우,
453해당 문서를 삭제할 수 있습니다. 예:</p>
454
455<pre>
456DocumentsContract.deleteDocument(getContentResolver(), uri);
457</pre>
458
459<h3 id="edit">문서 편집하기</h3>
460
461<p>준비된 텍스트 문서를 편집하는 데 SAF를 사용할 수 있습니다.
462이 조각은
463{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 인텐트를 실행하며
464{@link android.content.Intent#CATEGORY_OPENABLE} 카테고리를 사용해
465열 수 있는 문서만 표시하도록 합니다. 이것을 한층 더 필터링하여 텍스트 파일만 표시하게 하려면 다음과 같이 합니다.</p>
466
467<pre>
468private static final int EDIT_REQUEST_CODE = 44;
469/**
470 * Open a file for writing and append some text to it.
471 */
472 private void editDocument() {
473    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
474    // file browser.
475    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
476
477    // Filter to only show results that can be &quot;opened&quot;, such as a
478    // file (as opposed to a list of contacts or timezones).
479    intent.addCategory(Intent.CATEGORY_OPENABLE);
480
481    // Filter to show only text files.
482    intent.setType(&quot;text/plain&quot;);
483
484    startActivityForResult(intent, EDIT_REQUEST_CODE);
485}
486</pre>
487
488<p>다음으로, {@link android.app.Activity#onActivityResult onActivityResult()}
489(<a href="#results">결과 처리</a> 참조)에서 코드를 호출하여 편집 작업을 수행하도록 하면 됩니다.
490다음 조각은 {@link android.content.ContentResolver}에서 {@link java.io.FileOutputStream}
491을 가져온 것입니다. 이것은 기본적으로 "쓰기" 모드를 사용합니다.
492필요한 최소 수량의 액세스만을 요청하는 것이 가장 좋으니 쓰기만 필요하다면
493읽기/쓰기를 요청하지 마십시오.</p>
494
495<pre>private void alterDocument(Uri uri) {
496    try {
497        ParcelFileDescriptor pfd = getActivity().getContentResolver().
498                openFileDescriptor(uri, "w");
499        FileOutputStream fileOutputStream =
500                new FileOutputStream(pfd.getFileDescriptor());
501        fileOutputStream.write(("Overwritten by MyCloud at " +
502                System.currentTimeMillis() + "\n").getBytes());
503        // Let the document provider know you're done by closing the stream.
504        fileOutputStream.close();
505        pfd.close();
506    } catch (FileNotFoundException e) {
507        e.printStackTrace();
508    } catch (IOException e) {
509        e.printStackTrace();
510    }
511}</pre>
512
513<h3 id="permissions">권한 유지</h3>
514
515<p>앱이 읽기 또는 쓰기 작업에 대한 파일을 열면 시스템이 앱에 해당 파일에 대한 URI 권한 허가를
516부여합니다. 이것은 사용자의 장치를 다시 시작할 때까지 유지됩니다.
517하지만 만일 앱이 이미지 편집 앱이고, 사용자가 여러분의 앱에서 바로 편집한 5개의 이미지에
518액세스할 수 있도록 하고자 한다고 가정해봅시다. 사용자의 기기가 재시작되면
519여러분이 사용자에게 시스템 선택기를 다시 보내 해당 파일을 검색하도록 해야 할 텐데,
520이것은 물론 이상적인 것과는 거리가 멉니다.</p>
521
522<p>이런 일이 일어나지 않도록 방지하기 위해 시스템이 앱에 부여한 권한을 유지할 수 있습니다.
523여러분의 앱은 시스템이 제공하는 유지 가능한 URI 권한 허가를
524효율적으로 "받아들입니다". 이렇게 하면 사용자가 여러분의 앱을 통해 파일에 지속적인 액세스 권한을 가질 수 있으며,
525이는 기기가 다시 시작되더라도 관계 없습니다.</p>
526
527
528<pre>final int takeFlags = intent.getFlags()
529            &amp; (Intent.FLAG_GRANT_READ_URI_PERMISSION
530            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
531// Check for the freshest data.
532getContentResolver().takePersistableUriPermission(uri, takeFlags);</pre>
533
534<p>마지막 한 단계가 남았습니다. 여러분의 앱이 액세스한 가장 최근의 URI를
535저장해두었을 수 있지만, 이는 더 이상 유효하지 않을 수 있습니다. 또 다른 앱이 문서를
536삭제하거나 수정했을 수 있기 때문입니다. 따라서, 항상
537{@code getContentResolver().takePersistableUriPermission()}을
538호출하여 최신 데이터를 확인해야 합니다.</p>
539
540<h2 id="custom">사용자 지정 문서 제공자 작성하기</h2>
541
542<p>
543파일용 저장소 서비스를 제공하는 앱을 개발 중인 경우(예를 들어
544클라우드 저장 서비스 등), SAF를 통해 파일을 사용할 수 있도록 하려면 사용자 지정 문서 제공자를
545작성하면 됩니다.  이 섹션에서는 이렇게 하는 방법을 설명합니다.
546</p>
547
548
549<h3 id="manifest">매니페스트</h3>
550
551<p>사용자 지정 문서 제공자를 구현하려면 애플리케이션의 매니페스트에 다음과 같은 항목을
552추가하십시오.</p>
553<ul>
554
555<li>API 레벨 19 이상의 대상.</li>
556
557<li>사용자 지정 저장소 제공자를 선언하는 <code>&lt;provider&gt;</code>
558요소. </li>
559
560<li>제공자의 이름은 그 클래스 이름이며 여기에 패키지 이름도 포함됩니다.
561예: <code>com.example.android.storageprovider.MyCloudProvider</code></li>
562
563<li>권한의 이름, 이는 패키지 이름과 같으며(이 예시에서는
564<code>com.example.android.storageprovider</code>)여기에 콘텐츠 제공자 유형을 더합니다
565(<code>documents</code>). 예: {@code com.example.android.storageprovider.documents}</li>
566
567<li><code>android:exported</code> 속성을 <code>&quot;true&quot;</code>로 설정.
568제공자를 내보내 다른 앱이 볼 수 있도록 해야 합니다.</li>
569
570<li><code>android:grantUriPermissions</code> 속성을
571<code>&quot;true&quot;</code>로 설정. 이렇게 설정하면 시스템이 여러분의 제공자 안에 있는 콘텐츠에 액세스하도록 다른 앱에
572권한을 허가할 수 있게 해줍니다. 특정 문서에 대한 권한 부여를 유지하는 방법에 대한 논의는
573<a href="#permissions">권한 유지</a>를 참조하십시오.</li>
574
575<li>{@code MANAGE_DOCUMENTS} 권한. 기본적으로 제공자는 누구나 이용할 수 있습니다.
576 이 권한을 추가하면 여러분의 제공자를 시스템에 제한하게 됩니다.
577이 제한은 보안상 매우 중요합니다.</li>
578
579<li>{@code android:enabled} 속성을 리소스 파일에서 정의한 부울 값으로
580설정합니다. 이 속성의 목적은 Android 4.3 이하에서 실행되는 기기에서 제공자를 비활성화하는 데 있습니다.
581예를 들어 {@code android:enabled="@bool/atLeastKitKat"} 등입니다. 이 속성을
582매니페스트에 추가하는 것 이외에도 다음과 같은 작업을 해야 합니다.
583<ul>
584<li>{@code res/values/} 아래의 {@code bool.xml} 리소스 파일에
585이 라인을 추가합니다. <pre>&lt;bool name=&quot;atLeastKitKat&quot;&gt;false&lt;/bool&gt;</pre></li>
586
587<li>{@code res/values-v19/} 아래의 {@code bool.xml} 리소스 파일에
588이 라인을 추가합니다. <pre>&lt;bool name=&quot;atLeastKitKat&quot;&gt;true&lt;/bool&gt;</pre></li>
589</ul></li>
590
591<li>
592{@code android.content.action.DOCUMENTS_PROVIDER} 동작을 포함한 인텐트 필터가 있어야
593시스템이 제공자를 검색할 때 여러분의 제공자가 선택기에 나타날 수 있습니다.</li>
594
595</ul>
596<p>다음은 제공자를 포함한 샘플 매니페스트에서 발췌한 것입니다.</p>
597
598<pre>&lt;manifest... &gt;
599    ...
600    &lt;uses-sdk
601        android:minSdkVersion=&quot;19&quot;
602        android:targetSdkVersion=&quot;19&quot; /&gt;
603        ....
604        &lt;provider
605            android:name=&quot;com.example.android.storageprovider.MyCloudProvider&quot;
606            android:authorities=&quot;com.example.android.storageprovider.documents&quot;
607            android:grantUriPermissions=&quot;true&quot;
608            android:exported=&quot;true&quot;
609            android:permission=&quot;android.permission.MANAGE_DOCUMENTS&quot;
610            android:enabled=&quot;&#64;bool/atLeastKitKat&quot;&gt;
611            &lt;intent-filter&gt;
612                &lt;action android:name=&quot;android.content.action.DOCUMENTS_PROVIDER&quot; /&gt;
613            &lt;/intent-filter&gt;
614        &lt;/provider&gt;
615    &lt;/application&gt;
616
617&lt;/manifest&gt;</pre>
618
619<h4 id="43">Android 4.3 이하에서 실행되는 기기 지원</h4>
620
621<p>
622{@link android.content.Intent#ACTION_OPEN_DOCUMENT} 인텐트는
623Android 4.4 이상에서 실행되는 기기에서만 사용할 수 있습니다.
624애플리케이션이 {@link android.content.Intent#ACTION_GET_CONTENT}를 지원하도록 하여
625Android 4.3 이하에서 실행되는 기기에도 적용되도록 하려면 Android 4.4 이상에서 실행되는 기기용 매니페스트에 있는
626{@link android.content.Intent#ACTION_GET_CONTENT}
627 인텐트 필터를 비활성화해야 합니다.
628문서 제공자와 {@link android.content.Intent#ACTION_GET_CONTENT}는 상호 배타적인 것으로
629간주해야 합니다. 둘을 모두 동시에 지원하는 경우, 앱이 시스템 선택기 UI에
630두 번 나타나 저장된 데이터에 액세스할 두 가지 서로 다른 방법을 제안하게 됩니다.
631 이렇게 되면 사용자에게 혼동을 주게 되겠죠.</p>
632
633<p>다음은 Android 버전 4.4 이상에서 실행되는 기기용
634{@link android.content.Intent#ACTION_GET_CONTENT} 인텐트 필터를
635비활성화하는 데 권장되는 방법입니다.</p>
636
637<ol>
638<li>{@code res/values/} 아래의 {@code bool.xml} 리소스 파일에
639이 라인을 추가합니다. <pre>&lt;bool name=&quot;atMostJellyBeanMR2&quot;&gt;true&lt;/bool&gt;</pre></li>
640
641<li>{@code res/values-v19/} 아래의 {@code bool.xml} 리소스 파일에
642이 라인을 추가합니다. <pre>&lt;bool name=&quot;atMostJellyBeanMR2&quot;&gt;false&lt;/bool&gt;</pre></li>
643
644<li>
645<a href="{@docRoot}guide/topics/manifest/activity-alias-element.html">액티비티
646별칭</a>을 추가하여 버전 4.4(API 레벨 19) 이상을 대상으로 한 {@link android.content.Intent#ACTION_GET_CONTENT}
647인텐트 필터를 비활성화합니다. 예:
648
649<pre>
650&lt;!-- This activity alias is added so that GET_CONTENT intent-filter
651     can be disabled for builds on API level 19 and higher. --&gt;
652&lt;activity-alias android:name=&quot;com.android.example.app.MyPicker&quot;
653        android:targetActivity=&quot;com.android.example.app.MyActivity&quot;
654        ...
655        android:enabled=&quot;@bool/atMostJellyBeanMR2&quot;&gt;
656    &lt;intent-filter&gt;
657        &lt;action android:name=&quot;android.intent.action.GET_CONTENT&quot; /&gt;
658        &lt;category android:name=&quot;android.intent.category.OPENABLE&quot; /&gt;
659        &lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&gt;
660        &lt;data android:mimeType=&quot;image/*&quot; /&gt;
661        &lt;data android:mimeType=&quot;video/*&quot; /&gt;
662    &lt;/intent-filter&gt;
663&lt;/activity-alias&gt;
664</pre>
665</li>
666</ol>
667<h3 id="contract">계약</h3>
668
669<p>사용자 지정 제공자를 작성할 때면 일반적으로 수반되는 작업 중 하나가
670계약 클래스를 구현하는 것입니다. 이는
671<a href="{@docRoot}guide/topics/providers/content-provider-creating.html#ContractClass">
672콘텐츠 제공자</a> 개발자 가이드에서 설명한 것과 같습니다. 계약 클래스는 {@code public final} 클래스로,
673이 안에 URI에 대한 상수 정의, 열 이름, MIME 유형 및 제공자에 관련된
674다른 메타 데이터가 들어 있습니다. SAF가
675이와 같은 계약 클래스를 대신 제공해주므로 직접 쓰지 않아도
676됩니다.</p>
677
678<ul>
679   <li>{@link android.provider.DocumentsContract.Document}</li>
680   <li>{@link android.provider.DocumentsContract.Root}</li>
681</ul>
682
683<p>예를 들어 다음은 여러분의 문서 제공자가 문서 또는 루트에 대해 쿼리된 경우
684커서로 반환할 수 있는 열을 나타낸 것입니다.</p>
685
686<pre>private static final String[] DEFAULT_ROOT_PROJECTION =
687        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
688        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
689        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
690        Root.COLUMN_AVAILABLE_BYTES,};
691private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
692        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
693        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
694        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};
695</pre>
696
697<h3 id="subclass">하위 클래스 DocumentsProvider</h3>
698
699<p>사용자 지정 문서 제공자를 작성하기 위한 다음 단계는
700추상 클래스 {@link android.provider.DocumentsProvider}를 하위 클래스로 만드는 것입니다. 최소한 다음과 같은 메서드를 구현해야 합니다.
701</p>
702
703<ul>
704<li>{@link android.provider.DocumentsProvider#queryRoots queryRoots()}</li>
705
706<li>{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}</li>
707
708<li>{@link android.provider.DocumentsProvider#queryDocument queryDocument()}</li>
709
710<li>{@link android.provider.DocumentsProvider#openDocument openDocument()}</li>
711</ul>
712
713<p>꼭 구현해야만 하는 메서드는 이들뿐이지만, 개발자 여러분이 구현하고자 하는 메서드는 이보다
714훨씬 많을 수도 있습니다. 자세한 내용은{@link android.provider.DocumentsProvider}
715를 참조하십시오.</p>
716
717<h4 id="queryRoots">QueryRoots 구현</h4>
718
719<p>{@link android.provider.DocumentsProvider#queryRoots
720queryRoots()} 구현은 반드시 {@link android.database.Cursor}를 반환해야 하며,
721이는 문서 제공자의 모든 루트 디렉터리를 가리켜야 합니다. 이때
722{@link android.provider.DocumentsContract.Root}에서 정의한 열을 사용합니다.</p>
723
724<p>다음 조각에서 {@code projection} 매개변수는
725발신자가 돌려받고자 하는 특정 필드를 나타냅니다. 이 조각은 새 커서를 생성하며
726그에 하나의 행을 추가합니다. 하나의 루트,
727다운로드 또는 이미지와 같은 최상위 레벨 디렉터리가 해당됩니다.  대부분의 제공자에는 루트가 하나뿐입니다. 하나 이상이 있을 수도 있습니다.
728예를 들어 사용자 계정이 여러 개인 경우가 있습니다. 그런 경우에는 커서에 두 번째 행을
729추가하면 됩니다.</p>
730
731<pre>
732&#64;Override
733public Cursor queryRoots(String[] projection) throws FileNotFoundException {
734
735    // Create a cursor with either the requested fields, or the default
736    // projection if "projection" is null.
737    final MatrixCursor result =
738            new MatrixCursor(resolveRootProjection(projection));
739
740    // If user is not logged in, return an empty root cursor.  This removes our
741    // provider from the list entirely.
742    if (!isUserLoggedIn()) {
743        return result;
744    }
745
746    // It's possible to have multiple roots (e.g. for multiple accounts in the
747    // same app) -- just add multiple cursor rows.
748    // Construct one row for a root called &quot;MyCloud&quot;.
749    final MatrixCursor.RowBuilder row = result.newRow();
750    row.add(Root.COLUMN_ROOT_ID, ROOT);
751    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));
752
753    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
754    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
755    // recently used documents will show up in the &quot;Recents&quot; category.
756    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
757    // shares.
758    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
759            Root.FLAG_SUPPORTS_RECENTS |
760            Root.FLAG_SUPPORTS_SEARCH);
761
762    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
763    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));
764
765    // This document id cannot change once it's shared.
766    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));
767
768    // The child MIME types are used to filter the roots and only present to the
769    //  user roots that contain the desired type somewhere in their file hierarchy.
770    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
771    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
772    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);
773
774    return result;
775}</pre>
776
777<h4 id="queryChildDocuments">QueryChildDocuments 구현</h4>
778
779<p>
780{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}
781 구현은 반드시 {@link android.database.Cursor}를 반환해야 하며,
782이는 지정된 디렉터리 내의 모든 파일을 가리켜야 합니다. 이때
783{@link android.provider.DocumentsContract.Document}에서 정의한 열을 사용합니다.</p>
784
785<p>이 메서드는 선택기 UI에서 애플리케이션 루트를 선택하는 경우 호출됩니다.
786이는 해당 루트 아래 디렉터리의 하위 문서를 가져옵니다.  이것은 루트에서뿐만 아니라 파일 계층의 어느 레벨에서나
787호출할 수 있습니다. 이 조각은 요청한 열로 새 커서를 만든 다음,
788상위 디렉터리에 있는 모든 직속 하위에 대한 정보를 커서에 추가합니다.
789하위는 이미지, 또 다른 디렉터리가 될 수도 있고
790어느 파일이라도 될 수 있습니다.</p>
791
792<pre>&#64;Override
793public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
794                              String sortOrder) throws FileNotFoundException {
795
796    final MatrixCursor result = new
797            MatrixCursor(resolveDocumentProjection(projection));
798    final File parent = getFileForDocId(parentDocumentId);
799    for (File file : parent.listFiles()) {
800        // Adds the file's display name, MIME type, size, and so on.
801        includeFile(result, null, file);
802    }
803    return result;
804}
805</pre>
806
807<h4 id="queryDocument">QueryDocuments 구현</h4>
808
809<p>
810{@link android.provider.DocumentsProvider#queryDocument queryDocument()}
811 구현은 반드시 {@link android.database.Cursor}를 반환해야 하며,
812이는 지정된 파일을 가리켜야 합니다. 이때 {@link android.provider.DocumentsContract.Document}에서 정의한 열을 사용합니다.
813</p>
814
815<p>{@link android.provider.DocumentsProvider#queryDocument queryDocument()}
816 메서드는
817{@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}에서
818전달된 것과 같은 정보를 반환하지만, 특정한 파일에만 해당됩니다.</p>
819
820
821<pre>&#64;Override
822public Cursor queryDocument(String documentId, String[] projection) throws
823        FileNotFoundException {
824
825    // Create a cursor with the requested projection, or the default projection.
826    final MatrixCursor result = new
827            MatrixCursor(resolveDocumentProjection(projection));
828    includeFile(result, documentId, null);
829    return result;
830}
831</pre>
832
833<h4 id="openDocument">OpenDocument 구현</h4>
834
835<p>지정된 파일을 나타내는
836{@link android.os.ParcelFileDescriptor}를 반환하려면 {@link android.provider.DocumentsProvider#openDocument
837openDocument()}를 구현해야 합니다. 다른 앱들이 반환된 {@link android.os.ParcelFileDescriptor}를
838 사용하여 데이터를 스트리밍할 수 있습니다. 시스템은 사용자가 파일을 선택하고
839클라이언트 앱이 이에 대한 액세스를 요청하면서
840{@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}를 사용할 때 이 메서드를 호출합니다.
841예를 들면 다음과 같습니다.</p>
842
843<pre>&#64;Override
844public ParcelFileDescriptor openDocument(final String documentId,
845                                         final String mode,
846                                         CancellationSignal signal) throws
847        FileNotFoundException {
848    Log.v(TAG, &quot;openDocument, mode: &quot; + mode);
849    // It's OK to do network operations in this method to download the document,
850    // as long as you periodically check the CancellationSignal. If you have an
851    // extremely large file to transfer from the network, a better solution may
852    // be pipes or sockets (see ParcelFileDescriptor for helper methods).
853
854    final File file = getFileForDocId(documentId);
855
856    final boolean isWrite = (mode.indexOf('w') != -1);
857    if(isWrite) {
858        // Attach a close listener if the document is opened in write mode.
859        try {
860            Handler handler = new Handler(getContext().getMainLooper());
861            return ParcelFileDescriptor.open(file, accessMode, handler,
862                        new ParcelFileDescriptor.OnCloseListener() {
863                &#64;Override
864                public void onClose(IOException e) {
865
866                    // Update the file with the cloud server. The client is done
867                    // writing.
868                    Log.i(TAG, &quot;A file with id &quot; +
869                    documentId + &quot; has been closed!
870                    Time to &quot; +
871                    &quot;update the server.&quot;);
872                }
873
874            });
875        } catch (IOException e) {
876            throw new FileNotFoundException(&quot;Failed to open document with id &quot;
877            + documentId + &quot; and mode &quot; + mode);
878        }
879    } else {
880        return ParcelFileDescriptor.open(file, accessMode);
881    }
882}
883</pre>
884
885<h3 id="security">보안</h3>
886
887<p>여러분의 문서 제공자가 암호로 보호된 클라우드 저장소 서비스이고
888여러분은 사용자가 파일을 공유하기 전에 우선 로그인부터 하도록 확실히 해두고 싶다고 가정합니다.
889사용자가 로그인되지 않은 경우 앱은 어떻게 해야 합니까?  해법은
890{@link android.provider.DocumentsProvider#queryRoots
891queryRoots()} 구현에서 루트를 반환하지 않는 것입니다. 다시 말해, 텅 빈 루트 커서를 반환하는 것입니다.</p>
892
893<pre>
894public Cursor queryRoots(String[] projection) throws FileNotFoundException {
895...
896    // If user is not logged in, return an empty root cursor.  This removes our
897    // provider from the list entirely.
898    if (!isUserLoggedIn()) {
899        return result;
900}
901</pre>
902
903<p>또 다른 단계는 {@code getContentResolver().notifyChange()}를 호출하는 것입니다.
904{@link android.provider.DocumentsContract}를 기억하십니까?  이것을 사용하는 이유는
905바로 이 URI를 만들기 위해서입니다. 다음 조각은 사용자의 로그인 상태가 변경될 때마다
906시스템이 문서 제공자의 루트를 쿼리하도록 지시하고 있습니다. 사용자가 로그인되어 있지 않은 상태에서
907{@link android.provider.DocumentsProvider#queryRoots queryRoots()}를 호출하면
908위에서 나타낸 것과 같이 빈 커서를 반환합니다. 이렇게 하면 사용자가 제공자에 로그인되었을 때만
909제공자의 문서를 사용할 수 있도록 보장할 수 있습니다.</p>
910
911<pre>private void onLoginButtonClick() {
912    loginOrLogout();
913    getContentResolver().notifyChange(DocumentsContract
914            .buildRootsUri(AUTHORITY), null);
915}
916</pre>