맞춤 문서 제공자 만들기

파일 (예: 클라우드 저장 서비스인 경우 맞춤 문서 제공자를 작성하여 저장소 액세스 프레임워크 (SAF) 이 페이지에서는 맞춤 문서 제공자를 만드는 방법에 관해 설명합니다.

저장소 액세스 프레임워크의 작동 방식에 관한 자세한 내용은 저장소 액세스 프레임워크 개요

매니페스트

맞춤 문서 제공자를 구현하려면 애플리케이션의 매니페스트:

  • API 레벨 19 이상인 타겟.
  • 맞춤 저장소를 선언하는 <provider> 요소 제공업체
  • android:name 속성을 DocumentsProvider 서브클래스 패키지 이름을 포함한 클래스 이름입니다.

    com.example.android.storageprovider.MyCloudProvider.

  • android:authority 속성 패키지 이름입니다 (이 예에서는 com.example.android.storageprovider) 콘텐츠 제공자 유형을 (documents)
  • "true"로 설정한 android:exported 속성. 다른 앱에서 볼 수 있도록 제공업체를 내보내야 합니다.
  • android:grantUriPermissions 속성을 다음으로 설정 "true"입니다. 이 설정을 사용하면 시스템에서 다른 앱에 액세스 권한을 부여할 수 있습니다. 있습니다. 이러한 다른 앱이 계속 액세스할 수 있습니다. 지속 권한이 있는지 확인합니다.
  • MANAGE_DOCUMENTS 권한. 기본적으로 공급업체는 모든 사용자에게 공개합니다. 이 권한을 추가하면 제공자가 시스템으로 제한됩니다. 이 제한은 보안에 중요합니다.
  • 인텐트 필터에는 android.content.action.DOCUMENTS_PROVIDER 작업을 구성하여 제공자가 시스템이 제공자를 검색하면 선택 도구에 표시됩니다.

다음은 제공자가 포함된 샘플 manifest에서 발췌한 것입니다.

<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">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

Android 4.3 이하를 실행하는 지원 기기

ACTION_OPEN_DOCUMENT 인텐트만 사용 가능 Android 4.4 이상을 실행하는 기기에서 지원됩니다. 애플리케이션에서 ACTION_GET_CONTENT를 지원하려는 경우 Android 4.3 이하를 실행하는 기기를 수용하려면 다음에서 ACTION_GET_CONTENT 인텐트 필터를 사용 중지합니다. Android 4.4 이상을 실행하는 기기용 매니페스트를 다운로드하세요. 가 문서 제공자 및 ACTION_GET_CONTENT를 고려해야 함 할 수 있습니다. 두 가지를 동시에 지원하는 경우 시스템 선택 도구 UI에 두 번 표시되어 서로 다른 두 가지 액세스 방법을 제공합니다. 저장할 수 있습니다 이는 사용자에게 혼동을 줍니다.

이러한 차단 기능을 사용 중지하는 권장 방법은 기기용 ACTION_GET_CONTENT 인텐트 필터 Android 버전 4.4 이상을 실행하는 경우:

  1. res/values/ 아래의 bool.xml 리소스 파일에 다음 줄:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. res/values-v19/ 아래의 bool.xml 리소스 파일에 다음 줄:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 활동 별칭: ACTION_GET_CONTENT 인텐트를 사용 중지합니다. 버전 4.4 (API 수준 19) 이상을 위한 필터 예를 들면 다음과 같습니다.
    <!-- 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>
    

계약

일반적으로 맞춤 콘텐츠 제공자를 작성할 때 작업 중 하나는 구현 방법에 대해 자세히 알아보려면 <ph type="x-smartling-placeholder"></ph> 콘텐츠 제공업체 개발자 가이드를 참조하세요. 계약 클래스는 public final 클래스임 URI, 열 이름, MIME 유형 및 공급자와 관련된 기타 메타데이터입니다. SAF 이러한 계약 클래스가 자동으로 제공되므로 소유:

예를 들어, 문서 제공자가 문서 또는 루트에 대해 쿼리됩니다.

Kotlin

private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Root.COLUMN_ROOT_ID,
        DocumentsContract.Root.COLUMN_MIME_TYPES,
        DocumentsContract.Root.COLUMN_FLAGS,
        DocumentsContract.Root.COLUMN_ICON,
        DocumentsContract.Root.COLUMN_TITLE,
        DocumentsContract.Root.COLUMN_SUMMARY,
        DocumentsContract.Root.COLUMN_DOCUMENT_ID,
        DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
)
private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Document.COLUMN_DOCUMENT_ID,
        DocumentsContract.Document.COLUMN_MIME_TYPE,
        DocumentsContract.Document.COLUMN_DISPLAY_NAME,
        DocumentsContract.Document.COLUMN_LAST_MODIFIED,
        DocumentsContract.Document.COLUMN_FLAGS,
        DocumentsContract.Document.COLUMN_SIZE
)

자바

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의 서브클래스 만들기

맞춤 문서 제공자를 작성하는 다음 단계는 추상 클래스 DocumentsProvider. 최소한 다음 메서드를 구현합니다.

반드시 구현해야 하는 메서드는 이것뿐이지만 더 많은 것을 볼 수 있습니다. DocumentsProvider를 참고하세요. 참조하세요.

루트 정의

queryRoots() 구현은 모든 항목을 가리키는 Cursor를 반환해야 합니다. 문서 제공자의 루트 디렉터리에서 DocumentsContract.Root입니다.

다음 스니펫에서 projection 매개변수는 특정 필드를 지정해야 합니다. 스니펫은 새 커서를 만듭니다. 한 개의 행(하나의 루트, 최상위 수준 디렉토리)을 다운로드 또는 이미지. 대부분의 제공자에는 하나의 루트만 있습니다. 두 개 이상의 예를 들어 여러 사용자 계정의 경우 이 경우 두 번째 행을 커서로 가져갑니다.

Kotlin

override fun queryRoots(projection: Array<out String>?): Cursor {
    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    val result = 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.
    result.newRow().apply {
        add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)

        // You can provide an optional summary, which helps distinguish roots
        // with the same title. You can also use this field for displaying an
        // user account name.
        add(DocumentsContract.Root.COLUMN_SUMMARY, context.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.
        add(
            DocumentsContract.Root.COLUMN_FLAGS,
            DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
                DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
                DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
        )

        // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
        add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title))

        // This document id cannot change after it's shared.
        add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))

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

    return result
}

자바

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

    // Use a MatrixCursor to build 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.
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);

    // You can provide an optional summary, which helps distinguish roots
    // with the same title. You can also use this field for displaying an
    // user account name.
    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 after it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir));

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

    return result;
}

문서 제공자가 동적 루트 집합(예: USB 저장소)에 연결하는 경우 연결이 끊어졌을 수 있는 기기 또는 사용자가 로그아웃할 수 있는 계정 문서 UI를 업데이트하여 이러한 변경사항과 동기화 상태를 유지할 수 있습니다. ContentResolver.notifyChange() 메서드에 전달합니다.

Kotlin

val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY)
context.contentResolver.notifyChange(rootsUri, null)

자바

Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY);
context.getContentResolver().notifyChange(rootsUri, null);

제공자에 문서 나열

귀하가 구현한 queryChildDocuments() 모든 파일을 가리키는 Cursor을 반환해야 합니다. 여기에 정의된 열을 사용하여 DocumentsContract.Document입니다.

사용자가 선택도구 UI에서 루트를 선택하면 이 메서드가 호출됩니다. 이 메서드는 COLUMN_DOCUMENT_ID 그러면 사용자가 하위 디렉토리를 만듭니다.

이 스니펫은 요청된 열로 새 커서를 만든 다음 커서에 상위 디렉터리의 모든 직계 하위 요소에 대한 정보를 제공합니다. 하위 요소는 이미지, 다른 디렉터리 즉, 다음과 같이 모든 파일이 될 수 있습니다.

Kotlin

override fun queryChildDocuments(
        parentDocumentId: String?,
        projection: Array<out String>?,
        sortOrder: String?
): Cursor {
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        val parent: File = getFileForDocId(parentDocumentId)
        parent.listFiles()
                .forEach { file ->
                    includeFile(this, null, file)
                }
    }
}

자바

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

문서 정보 가져오기

귀하가 구현한 queryDocument() 지정된 파일을 가리키는 Cursor를 반환해야 합니다. DocumentsContract.Document에 정의된 열 사용

queryDocument() 메서드는 queryChildDocuments(), 특정 파일의 경우:

Kotlin

override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
    // Create a cursor with the requested projection, or the default projection.
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        includeFile(this, documentId, null)
    }
}

자바

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

문서 제공자는 또한 재정의하면 DocumentsProvider.openDocumentThumbnail() 메서드를 호출하고 FLAG_SUPPORTS_THUMBNAIL 플래그를 지원되는 파일에 추가합니다. 다음 코드 스니펫은 DocumentsProvider.openDocumentThumbnail()

Kotlin

override fun openDocumentThumbnail(
        documentId: String?,
        sizeHint: Point?,
        signal: CancellationSignal?
): AssetFileDescriptor {
    val file = getThumbnailFileForDocId(documentId)
    val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    return AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH)
}

자바

@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint,
                                                     CancellationSignal signal)
        throws FileNotFoundException {

    final File file = getThumbnailFileForDocId(documentId);
    final ParcelFileDescriptor pfd =
        ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
}

주의: 문서 제공자는 썸네일 이미지를 두 배 이상 반환해서는 안 됩니다. sizeHint 매개변수로 지정된 크기입니다.

문서 열기

openDocument()를 구현하여 다음을 나타내는 ParcelFileDescriptor를 반환해야 합니다. 지정합니다. 다른 앱에서 반환된 ParcelFileDescriptor를 사용할 수 있습니다. 데이터를 스트리밍할 수 있습니다 시스템은 사용자가 파일을 선택한 후 이 메서드를 호출합니다. 클라이언트 앱이 openFileDescriptor() 예를 들면 다음과 같습니다.

Kotlin

override fun openDocument(
        documentId: String,
        mode: String,
        signal: CancellationSignal
): ParcelFileDescriptor {
    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).

    val file: File = getFileForDocId(documentId)
    val accessMode: Int = ParcelFileDescriptor.parseMode(mode)

    val isWrite: Boolean = mode.contains("w")
    return if (isWrite) {
        val handler = Handler(context.mainLooper)
        // Attach a close listener if the document is opened in write mode.
        try {
            ParcelFileDescriptor.open(file, accessMode, handler) {
                // 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 (e: IOException) {
            throw FileNotFoundException(
                    "Failed to open document with id $documentId and mode $mode"
            )
        }
    } else {
        ParcelFileDescriptor.open(file, accessMode)
    }
}

자바

@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 int accessMode = ParcelFileDescriptor.parseMode(mode);

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

문서 제공자가 파일을 스트리밍하거나 복잡한 작업을 처리하는 경우 구현하려는 경우 createReliablePipe() 또는 createReliableSocketPair() 메서드와 함께 사용할 수 있습니다. 이러한 메서드를 사용하면 ParcelFileDescriptor 객체, 여기서 하나를 반환할 수 있음 다른 하나는 포드를 통해 ParcelFileDescriptor.AutoCloseOutputStream 또는 ParcelFileDescriptor.AutoCloseInputStream입니다.

최근 문서 지원 및 검색

루트 관리자 아래에 최근 수정한 문서 목록을 이 메서드를 재정의하여 queryRecentDocuments() 메서드 및 반환 FLAG_SUPPORTS_RECENTS, 다음 코드 스니펫은 queryRecentDocuments() 메서드를 사용하여 지도 가장자리에 패딩을 추가할 수 있습니다.

Kotlin

override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor {
    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    val result = MatrixCursor(resolveDocumentProjection(projection))

    val parent: File = getFileForDocId(rootId)

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    val lastModifiedFiles = PriorityQueue(
            5,
            Comparator<File> { i, j ->
                Long.compare(i.lastModified(), j.lastModified())
            }
    )

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    val pending : MutableList<File> = mutableListOf()

    // Start by adding the parent to the list of files to be processed
    pending.add(parent)

    // Do while we still have unexamined files
    while (pending.isNotEmpty()) {
        // Take a file from the list of unprocessed files
        val file: File = pending.removeAt(0)
        if (file.isDirectory) {
            // If it's a directory, add all its children to the unprocessed list
            pending += file.listFiles()
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file)
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) {
        val file: File = lastModifiedFiles.remove()
        includeFile(result, null, file)
    }
    return result
}

자바

@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
        throws FileNotFoundException {

    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

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

    final File parent = getFileForDocId(rootId);

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    PriorityQueue lastModifiedFiles =
        new PriorityQueue(5, new Comparator() {

        public int compare(File i, File j) {
            return Long.compare(i.lastModified(), j.lastModified());
        }
    });

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    final LinkedList pending = new LinkedList();

    // Start by adding the parent to the list of files to be processed
    pending.add(parent);

    // Do while we still have unexamined files
    while (!pending.isEmpty()) {
        // Take a file from the list of unprocessed files
        final File file = pending.removeFirst();
        if (file.isDirectory()) {
            // If it's a directory, add all its children to the unprocessed list
            Collections.addAll(pending, file.listFiles());
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file);
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) {
        final File file = lastModifiedFiles.remove();
        includeFile(result, null, file);
    }
    return result;
}

위 스니펫의 전체 코드를 가져오려면 StorageProvider 살펴보겠습니다

문서 생성 지원

클라이언트 앱을 사용하여 문서 제공자 내에 파일을 만들 수 있습니다. 클라이언트 앱이 ACTION_CREATE_DOCUMENT를 전송하는 경우 인텐트에 포함된 경우 문서 제공자는 클라이언트 앱에서 새 문서를 만들 수 있습니다

문서 생성을 지원하려면 루트에 FLAG_SUPPORTS_CREATE 플래그. 디렉터리 내에 새 파일을 만들 수 있도록 하려면 디렉터리에 FLAG_DIR_SUPPORTS_CREATE 플래그.

또한 문서 제공자는 createDocument() 메서드를 사용하여 지도 가장자리에 패딩을 추가할 수 있습니다. 사용자가 새 파일을 저장하기 위해 문서 제공자가 저장되면 문서 제공자는 createDocument() createDocument() 메서드를 사용하여 새 값을 반환하는 경우 COLUMN_DOCUMENT_ID: 파일에서 참조됩니다. 그러면 클라이언트 앱이 이 ID를 사용하여 파일의 핸들을 가져올 수 있습니다. 궁극적으로 openDocument(): 새 파일에 씁니다.

다음 코드 스니펫은 문서 제공자일 것입니다

Kotlin

override fun createDocument(documentId: String?, mimeType: String?, displayName: String?): String {
    val parent: File = getFileForDocId(documentId)
    val file: File = try {
        File(parent.path, displayName).apply {
            createNewFile()
            setWritable(true)
            setReadable(true)
        }
    } catch (e: IOException) {
        throw FileNotFoundException(
                "Failed to create document with name $displayName and documentId $documentId"
        )
    }

    return getDocIdForFile(file)
}

자바

@Override
public String createDocument(String documentId, String mimeType, String displayName)
        throws FileNotFoundException {

    File parent = getFileForDocId(documentId);
    File file = new File(parent.getPath(), displayName);
    try {
        file.createNewFile();
        file.setWritable(true);
        file.setReadable(true);
    } catch (IOException e) {
        throw new FileNotFoundException("Failed to create document with name " +
                displayName +" and documentId " + documentId);
    }
    return getDocIdForFile(file);
}

위 스니펫의 전체 코드를 가져오려면 StorageProvider 살펴보겠습니다

문서 관리 기능 지원

문서 제공자는 파일을 열고, 만들고, 보는 것 외에도 또한 클라이언트 앱이 사용자 이름과 비밀번호를 할 수 있습니다. 문서 관리 기능을 추가하려면 원하는 경우 문서의 열 COLUMN_FLAGS개 지원되는 기능을 나타냅니다. 또한 인코더-디코더 아키텍처를 DocumentsProvider의 상응하는 메서드 클래스에 대해 자세히 알아보세요.

다음 표에서는 COLUMN_FLAGS 플래그 및 DocumentsProvider 메서드를 사용하여 제공자가 특정 기능을 노출하기 위해 구현해야 합니다.

기능 플래그 메서드
파일 삭제 FLAG_SUPPORTS_DELETE deleteDocument()
파일 이름 바꾸기 FLAG_SUPPORTS_RENAME renameDocument()
문서 제공자 내의 새 상위 디렉터리에 파일 복사 FLAG_SUPPORTS_COPY copyDocument()
문서 제공자 내의 한 디렉터리에서 다른 디렉터리로 파일 이동 FLAG_SUPPORTS_MOVE moveDocument()
상위 디렉터리에서 파일 삭제 FLAG_SUPPORTS_REMOVE removeDocument()

가상 파일 및 대체 파일 형식 지원

가상 파일, Android 7.0 (API 수준 24)에 도입된 기능으로 권한이 없는 파일에 대한 보기 권한을 직접 바이트 코드 표현을 사용합니다. 다른 앱에서 가상 파일을 볼 수 있도록 하려면 다음 안내를 따르세요. 문서 제공자가 열 수 있는 대체 파일을 생성해야 함 표현해야 합니다.

예를 들어, 문서 제공자에 다른 앱에서 직접 열 수 없는 형식이며 기본적으로 가상 파일입니다. 클라이언트 앱이 ACTION_VIEW 인텐트를 전송하는 경우 CATEGORY_OPENABLE 카테고리 없이 사용자는 문서 제공자 내에서 이러한 가상 파일을 선택할 수 있습니다. 볼 수 있습니다. 그러면 문서 제공자가 가상 파일을 반환하며 이미지와 같이 다른 파일 형식으로 열 수 있습니다. 그러면 클라이언트 앱은 가상 파일을 열어 사용자가 보게 할 수 있습니다.

제공자 내의 문서가 가상이라고 선언하려면 FLAG_VIRTUAL_DOCUMENT 플래그를 queryDocument() 메서드를 사용하여 축소하도록 요청합니다. 이 플래그는 파일에 직접적인 직접 열 수 없습니다.

문서 제공자의 파일이 가상이라고 선언하면 다른 동영상에서 사용할 수 있도록 하는 것이 이미지 또는 PDF와 같은 MIME 유형입니다. 문서 제공자 대체 MIME 유형을 선언합니다. 를 재정의하여 가상 파일 보기를 지원합니다. getDocumentStreamTypes() 메서드를 사용하여 축소하도록 요청합니다. 클라이언트 앱이 getStreamTypes(android.net.Uri, java.lang.String) 메서드가 호출되면 시스템은 getDocumentStreamTypes() 메서드를 호출할 수 있습니다. 이 getDocumentStreamTypes() 메서드가 기본 MIME 유형의 배열을 반환합니다. 문서 제공자가 파일에 대해 지원합니다.

클라이언트가 문서를 생성할 수 있는 파일 형식이며 형식일 때 클라이언트 앱이 openTypedAssetFileDescriptor() 메서드를 호출하여 문서 제공자의 openTypedDocument() 메서드를 사용하여 축소하도록 요청합니다. 문서 제공자는 다음 시간에 클라이언트 앱으로 파일을 반환합니다. 요청된 파일 형식입니다.

다음 코드 스니펫은 getDocumentStreamTypes()openTypedDocument() 메서드를 참조하세요.

Kotlin

var SUPPORTED_MIME_TYPES : Array<String> = arrayOf("image/png", "image/jpg")
override fun openTypedDocument(
        documentId: String?,
        mimeTypeFilter: String,
        opts: Bundle?,
        signal: CancellationSignal?
): AssetFileDescriptor? {
    return try {
        // Determine which supported MIME type the client app requested.
        when(mimeTypeFilter) {
            "image/jpg" -> openJpgDocument(documentId)
            "image/png", "image/*", "*/*" -> openPngDocument(documentId)
            else -> throw IllegalArgumentException("Invalid mimeTypeFilter $mimeTypeFilter")
        }
    } catch (ex: Exception) {
        Log.e(TAG, ex.message)
        null
    }
}

override fun getDocumentStreamTypes(documentId: String, mimeTypeFilter: String): Array<String> {
    return when (mimeTypeFilter) {
        "*/*", "image/*" -> {
            // Return all supported MIME types if the client app
            // passes in '*/*' or 'image/*'.
            SUPPORTED_MIME_TYPES
        }
        else -> {
            // Filter the list of supported mime types to find a match.
            SUPPORTED_MIME_TYPES.filter { it == mimeTypeFilter }.toTypedArray()
        }
    }
}

자바

public static String[] SUPPORTED_MIME_TYPES = {"image/png", "image/jpg"};

@Override
public AssetFileDescriptor openTypedDocument(String documentId,
    String mimeTypeFilter,
    Bundle opts,
    CancellationSignal signal) {

    try {

        // Determine which supported MIME type the client app requested.
        if ("image/png".equals(mimeTypeFilter) ||
            "image/*".equals(mimeTypeFilter) ||
            "*/*".equals(mimeTypeFilter)) {

            // Return the file in the specified format.
            return openPngDocument(documentId);

        } else if ("image/jpg".equals(mimeTypeFilter)) {
            return openJpgDocument(documentId);
        } else {
            throw new IllegalArgumentException("Invalid mimeTypeFilter " + mimeTypeFilter);
        }

    } catch (Exception ex) {
        Log.e(TAG, ex.getMessage());
    } finally {
        return null;
    }
}

@Override
public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) {

    // Return all supported MIME tyupes if the client app
    // passes in '*/*' or 'image/*'.
    if ("*/*".equals(mimeTypeFilter) ||
        "image/*".equals(mimeTypeFilter)) {
        return SUPPORTED_MIME_TYPES;
    }

    ArrayList requestedMimeTypes = new ArrayListl&t;g&t;();

    // Iterate over the list of supported mime types to find a match.
    for (int i=0; i l&t; SUPPORTED_MIME_TYPES.length; i++) {
        if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) {
            requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]);
        }
    }
    return (String[])requestedMimeTypes.toArray();
}

보안

문서 제공자가 비밀번호로 보호되는 클라우드 스토리지 서비스인 경우 파일 공유를 시작하기 전에 사용자가 로그인되어 있는지 확인하려고 합니다. 사용자가 로그인하지 않았다면 앱에서 어떻게 해야 할까요? 해결책은 queryRoots() 구현에서 루트가 0인 경우 즉, 다음과 같이 비어있는 루트 커서입니다.

Kotlin

override fun queryRoots(projection: Array<out String>): Cursor {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

자바

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

다른 방법은 getContentResolver().notifyChange()를 호출하는 것입니다. DocumentsContract를 기억하시나요? 이를 통해 이 URI를 반환합니다. 다음 스니펫은 시스템에 문서 제공자의 상태를 변경할 수 있습니다. 사용자가 queryRoots()을 호출하면 빈 커서를 반환합니다. 이렇게 하면 제공업체의 문서가 사용할 수 있습니다.

Kotlin

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

자바

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

이 페이지와 관련된 샘플 코드는 다음을 참조하세요.

이 페이지와 관련된 동영상은 다음을 참조하세요.

추가 관련 정보는 다음을 참조하세요.