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

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

ตัวอย่างเช่น คอลัมน์ต่อไปนี้อาจเป็นคอลัมน์ที่คุณอาจเห็นในเคอร์เซอร์เมื่อ ผู้ให้บริการเอกสารของคุณค้นหาเอกสารหรือรูท:

KotlinJava
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

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

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

กำหนดราก

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

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

KotlinJava
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() ดังที่แสดงในข้อมูลโค้ดต่อไปนี้

KotlinJava
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 จากนั้นระบบจะเรียกวิธีนี้ทุกครั้งที่ผู้ใช้เลือก ภายในผู้ให้บริการเอกสารของคุณ

ข้อมูลโค้ดนี้จะสร้างเคอร์เซอร์ใหม่โดยมีคอลัมน์ที่ขอ จากนั้นเพิ่ม ข้อมูลเกี่ยวกับรายการย่อยทั้งหมดที่อยู่ในไดเรกทอรีระดับบนสุดไปจนถึงเคอร์เซอร์ ย่อยอาจเป็นรูปภาพหรือไดเรกทอรีอื่นก็ได้ ดังนี้

KotlinJava
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(), แต่สำหรับไฟล์ที่เจาะจง

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

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

ข้อควรระวัง: ผู้ให้บริการเอกสารไม่ควรแสดงภาพขนาดย่อเกิน 2 เท่า ขนาดที่ระบุโดยพารามิเตอร์ sizeHint

เปิดเอกสาร

คุณต้องใช้ openDocument() เพื่อแสดง ParcelFileDescriptor ที่เป็นตัวแทน ไฟล์ที่ระบุ แอปอื่นๆ จะใช้ ParcelFileDescriptor ที่ส่งคืนได้ เพื่อสตรีมข้อมูล ระบบจะเรียกวิธีนี้หลังจากที่ผู้ใช้เลือกไฟล์ และแอปไคลเอ็นต์จะขอสิทธิ์เข้าถึงด้วยการเรียกใช้ openFileDescriptor() เช่น

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

รองรับการค้นหาเอกสารและล่าสุด

คุณสามารถระบุรายการของเอกสารที่แก้ไขล่าสุดภายใต้รูทของ ของผู้ให้บริการเอกสาร โดยลบล้าง queryRecentDocuments() วิธีและการส่งคืน FLAG_SUPPORTS_RECENTS, ข้อมูลโค้ดต่อไปนี้จะแสดงตัวอย่างวิธีการติดตั้ง queryRecentDocuments() วิธี

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

ข้อมูลโค้ดต่อไปนี้แสดงวิธีสร้างไฟล์ใหม่ภายใน ผู้ให้บริการเอกสาร

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

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

KotlinJava
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 ArrayList&lt;&gt;();

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

ความปลอดภัย

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

KotlinJava
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() แสดงผล เคอร์เซอร์ว่างเปล่า ดังที่แสดงด้านบน เพื่อให้มั่นใจว่าเอกสารของผู้ให้บริการ ใช้งานได้หากผู้ใช้เข้าสู่ระบบผู้ให้บริการ

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

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

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

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