หากคุณพัฒนาแอปที่ให้บริการพื้นที่เก็บข้อมูลสำหรับไฟล์ (เช่น บริการบันทึกในระบบคลาวด์) คุณสามารถทำให้ไฟล์พร้อมใช้งานผ่าน 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 หรือใหม่กว่า
- ในไฟล์ทรัพยากรของ
bool.xml
ภายใต้res/values/
ให้เพิ่ม บรรทัดนี้:<bool name="atMostJellyBeanMR2">true</bool>
- ในไฟล์ทรัพยากรของ
bool.xml
ภายใต้res/values-v19/
ให้เพิ่ม บรรทัดนี้:<bool name="atMostJellyBeanMR2">false</bool>
- เพิ่ม
กิจกรรม
ชื่อแทนเพื่อปิดใช้ 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,};
เคอร์เซอร์ของรากต้องมีคอลัมน์ที่จําเป็นบางรายการ คอลัมน์เหล่านี้ได้แก่
เคอร์เซอร์สำหรับเอกสารต้องมีคอลัมน์ที่จำเป็นต่อไปนี้
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
สร้างคลาสย่อยของ 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); }
ดูโค้ดตัวอย่างที่เกี่ยวข้องกับหน้านี้ได้ที่
สำหรับวิดีโอที่เกี่ยวข้องกับหน้านี้ โปรดดูที่
- DevBytes: เฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูลของ Android 4.4: ผู้ให้บริการ
- เฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูล: การสร้าง DocumentsProvider
- ไฟล์เสมือนในเฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูล
ดูข้อมูลที่เกี่ยวข้องเพิ่มเติมได้ที่
- การสร้าง DocumentsProvider
- เปิดไฟล์โดยใช้เฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูล
- ข้อมูลเบื้องต้นเกี่ยวกับผู้ให้บริการเนื้อหา