สร้างผู้ให้บริการเอกสารที่กำหนดเอง

หากคุณพัฒนาแอปที่ให้บริการพื้นที่เก็บข้อมูลสำหรับไฟล์ (เช่น บริการบันทึกในระบบคลาวด์) คุณสามารถทำให้ไฟล์พร้อมใช้งานผ่าน Storage Access Framework (SAF) โดยการเขียนผู้ให้บริการเอกสารที่กำหนดเอง หน้านี้จะอธิบายวิธีสร้างผู้ให้บริการเอกสารที่กำหนดเอง

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการทำงานของเฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูล โปรดดูที่ ภาพรวมเฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูล

ไฟล์ Manifest

หากต้องการใช้ผู้ให้บริการเอกสารที่กำหนดเอง ให้เพิ่มค่าต่อไปนี้ใน ไฟล์ Manifest:

  • เป้าหมาย API ระดับ 19 ขึ้นไป
  • องค์ประกอบ <provider> ที่ประกาศพื้นที่เก็บข้อมูลที่กำหนดเอง
  • ตั้งชื่อแอตทริบิวต์ android:name เป็นชื่อของ คลาสย่อย DocumentsProvider รายการ ซึ่งเป็นชื่อคลาสรวมถึงชื่อแพ็กเกจ:

    com.example.android.storageprovider.MyCloudProvider

  • แอตทริบิวต์ android:authority ซึ่งเป็นชื่อแพ็กเกจของคุณ (ในตัวอย่างนี้ com.example.android.storageprovider) รวมทั้งประเภทผู้ให้บริการเนื้อหา (documents)
  • ตั้งค่าแอตทริบิวต์ android:exported เป็น "true" แล้ว คุณต้องส่งออกผู้ให้บริการเพื่อให้แอปอื่นๆ สามารถดูได้
  • ตั้งค่าแอตทริบิวต์ android:grantUriPermissions เป็น "true" การตั้งค่านี้ช่วยให้ระบบให้สิทธิ์เข้าถึงแก่แอปอื่นๆ ได้ เนื้อหาในผู้ให้บริการ สำหรับการพูดคุยเกี่ยวกับวิธีที่แอปอื่นๆ เหล่านี้สามารถ ยืนยันสิทธิ์การเข้าถึงเนื้อหาจากผู้ให้บริการของคุณ โปรดดู คงไว้ สิทธิ์
  • สิทธิ์ MANAGE_DOCUMENTS โดยค่าเริ่มต้น ผู้ให้บริการจะพร้อมให้ใช้งาน สำหรับทุกคน การเพิ่มสิทธิ์นี้จะจำกัดให้ผู้ให้บริการของคุณเข้าสู่ระบบได้ ข้อจำกัดนี้มีความสำคัญต่อความปลอดภัย
  • ตัวกรอง Intent ที่มี 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 Intent ใช้งานได้เท่านั้น บนอุปกรณ์ที่ใช้ Android 4.4 ขึ้นไป หากคุณต้องการให้แอปพลิเคชันของคุณรองรับ ACTION_GET_CONTENT เพื่อรองรับอุปกรณ์ที่ใช้ Android 4.3 หรือต่ำกว่า ปิดใช้ตัวกรอง Intent ACTION_GET_CONTENT ใน ไฟล์ Manifest สำหรับอุปกรณ์ที่ใช้ Android 4.4 ขึ้นไป ต เป็นผู้ให้บริการเอกสารและ ACTION_GET_CONTENT แยกกันต่างหาก หากคุณรองรับทั้ง 2 ฟีเจอร์พร้อมกัน แอปของคุณ ปรากฏ 2 ครั้งใน UI เครื่องมือเลือกระบบ โดยมี 2 วิธีในการเข้าถึง ข้อมูลที่จัดเก็บของคุณ ซึ่งจะทำให้ผู้ใช้สับสน

ต่อไปนี้เป็นวิธีการปิดใช้งาน ตัวกรอง Intent ACTION_GET_CONTENT สำหรับอุปกรณ์ ที่ใช้ Android เวอร์ชัน 4.4 หรือใหม่กว่า

  1. ในไฟล์ทรัพยากรของ bool.xml ภายใต้ res/values/ ให้เพิ่ม บรรทัดนี้:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. ในไฟล์ทรัพยากรของ bool.xml ภายใต้ res/values-v19/ ให้เพิ่ม บรรทัดนี้:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. เพิ่ม กิจกรรม ชื่อแทนเพื่อปิดใช้ Intent ของ 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>
    

สัญญา

ปกติแล้วเมื่อคุณเขียนผู้ให้บริการเนื้อหาที่กำหนดเอง งานหนึ่งก็คือ การใช้คลาสสัญญา ตามที่อธิบายไว้ใน คู่มือนักพัฒนาซอฟต์แวร์สำหรับนักพัฒนาแอป คลาสสัญญาเป็นคลาส 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
)

Java

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

ขั้นตอนถัดไปในการเขียนผู้ให้บริการเอกสารที่กำหนดเองคือ คลาสย่อยของ Abstract Class DocumentsProvider อย่างน้อยที่สุด คุณต้อง ให้ใช้วิธีการต่อไปนี้

นี่คือวิธีการเดียวที่คุณจำเป็นต้องใช้อย่างเคร่งครัด ยังมีอีกมากที่คุณอาจต้องการ โปรดดู DocumentsProvider เพื่อดูรายละเอียด

กำหนดราก

การใช้งาน queryRoots() จะต้องแสดง Cursor ที่ชี้ไปยัง ไดเรกทอรีรากของผู้ให้บริการเอกสารของคุณ โดยใช้คอลัมน์ที่กำหนดไว้ใน DocumentsContract.Root

ในข้อมูลโค้ดต่อไปนี้ พารามิเตอร์ projection แสดงถึง ฟิลด์ที่เจาะจงซึ่งผู้โทรต้องการคืนค่า ข้อมูลโค้ดสร้างเคอร์เซอร์ใหม่ และเพิ่ม 1 แถวลงในแถวนั้น เช่น ราก 1 แถว ซึ่งเป็นไดเรกทอรีระดับบนสุด เช่น ดาวน์โหลดหรือรูปภาพ ผู้ให้บริการส่วนใหญ่มีรูทเพียงรายการเดียว อาจมีมากกว่า 1 รายการ เช่น ในกรณีที่มีบัญชีผู้ใช้หลายบัญชี ในกรณีนี้ ให้เพิ่ม แถวที่ 2 ของเคอร์เซอร์

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
}

Java

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

Java

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

Java

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

Java

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

Java

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

ข้อควรระวัง: ผู้ให้บริการเอกสารไม่ควรแสดงภาพขนาดย่อเกิน 2 เท่า ขนาดที่ระบุโดยพารามิเตอร์ 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)
    }
}

Java

@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 รายการ คุณส่งคืนออบเจ็กต์ได้ 1 รายการ และส่งอีกฝ่ายผ่าน วันที่ 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
}

Java

@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 สำหรับ จากนั้นแอปไคลเอ็นต์จะใช้รหัสนั้นเพื่อรับแฮนเดิลสำหรับไฟล์ได้ และท้ายที่สุดคือการเรียก 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)
}

Java

@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) ช่วยให้ผู้ให้บริการเอกสาร เพื่อให้สิทธิ์ในการดูไฟล์ที่ไม่มี การแสดงไบต์โค้ดโดยตรง หากต้องการทำให้แอปอื่นๆ ดูไฟล์เสมือนได้ ให้ทำดังนี้ ผู้ให้บริการเอกสารของคุณต้องสร้างไฟล์อื่นที่เปิดได้ ไฟล์เสมือนได้

ตัวอย่างเช่น สมมติว่าผู้ให้บริการเอกสารมีไฟล์ ที่แอปอื่นไม่สามารถเปิดได้โดยตรง ซึ่งก็คือไฟล์เสมือน เมื่อแอปไคลเอ็นต์ส่ง Intent ACTION_VIEW ที่ไม่มีหมวดหมู่ CATEGORY_OPENABLE ผู้ใช้สามารถเลือกไฟล์เสมือนเหล่านี้ภายในผู้ให้บริการเอกสาร จากนั้นผู้ให้บริการเอกสารจะส่งคืนไฟล์เสมือน ในรูปแบบไฟล์อื่นแต่เปิดได้ เช่น รูปภาพ จากนั้นแอปไคลเอ็นต์จะเปิดไฟล์เสมือนเพื่อให้ผู้ใช้ดูได้

หากต้องการประกาศว่าเอกสารในผู้ให้บริการเป็นแบบเสมือน คุณจะต้องเพิ่ม FLAG_VIRTUAL_DOCUMENT ไปยังไฟล์ที่ส่งคืนโดย วันที่ queryDocument() ธงนี้จะแจ้งเตือนแอปไคลเอ็นต์ว่าไฟล์ดังกล่าวไม่มี การแสดงไบต์โค้ดและไม่สามารถเปิดโดยตรงได้

หากคุณประกาศว่าไฟล์ในผู้ให้บริการเอกสารเป็นไฟล์เสมือน เราขอแนะนำให้คุณทำให้ AdSense พร้อมใช้งานใน ประเภท MIME เช่น รูปภาพหรือ PDF ผู้ให้บริการเอกสาร ประกาศประเภท 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()
        }
    }
}

Java

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() ซึ่งก็คือเคอร์เซอร์รากที่ว่างเปล่าดังนี้

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
    }

Java

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

Java

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

ดูโค้ดตัวอย่างที่เกี่ยวข้องกับหน้านี้ได้ที่

สำหรับวิดีโอที่เกี่ยวข้องกับหน้านี้ โปรดดูที่

ดูข้อมูลที่เกี่ยวข้องเพิ่มเติมได้ที่