หากคุณพัฒนาแอปที่ให้บริการพื้นที่เก็บข้อมูลสำหรับไฟล์ (เช่น บริการบันทึกในระบบคลาวด์) คุณสามารถทำให้ไฟล์พร้อมใช้งานผ่าน 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
ชั้นสัญญาเหล่านี้ให้กับคุณ คุณจึงไม่จำเป็นต้องเขียน
ของตัวเอง:
ตัวอย่างเช่น คอลัมน์ต่อไปนี้อาจเป็นคอลัมน์ที่คุณอาจเห็นในเคอร์เซอร์เมื่อ ผู้ให้บริการเอกสารของคุณค้นหาเอกสารหรือรูท:
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,};
เคอร์เซอร์ของรากต้องมีคอลัมน์ที่จําเป็นบางรายการ คอลัมน์เหล่านี้ได้แก่
เคอร์เซอร์สำหรับเอกสารต้องมีคอลัมน์ที่จำเป็นต่อไปนี้
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 ของเคอร์เซอร์
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()
ดังที่แสดงในข้อมูลโค้ดต่อไปนี้
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
จากนั้นระบบจะเรียกวิธีนี้ทุกครั้งที่ผู้ใช้เลือก
ภายในผู้ให้บริการเอกสารของคุณ
ข้อมูลโค้ดนี้จะสร้างเคอร์เซอร์ใหม่โดยมีคอลัมน์ที่ขอ จากนั้นเพิ่ม ข้อมูลเกี่ยวกับรายการย่อยทั้งหมดที่อยู่ในไดเรกทอรีระดับบนสุดไปจนถึงเคอร์เซอร์ ย่อยอาจเป็นรูปภาพหรือไดเรกทอรีอื่นก็ได้ ดังนี้
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()
,
แต่สำหรับไฟล์ที่เจาะจง
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()
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()
เช่น
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()
วิธี
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()
เพื่อเขียนลงในไฟล์ใหม่
ข้อมูลโค้ดต่อไปนี้แสดงวิธีสร้างไฟล์ใหม่ภายใน ผู้ให้บริการเอกสาร
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()
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<>();
// Iterate over the list of supported mime types to find a match.
for (int i=0; i < SUPPORTED_MIME_TYPES.length; i++) {
if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) {
requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]);
}
}
return (String[])requestedMimeTypes.toArray();
}
ความปลอดภัย
สมมติว่าผู้ให้บริการเอกสารเป็นบริการพื้นที่เก็บข้อมูลระบบคลาวด์ที่มีการป้องกันด้วยรหัสผ่าน
และคุณต้องการให้ผู้ใช้เข้าสู่ระบบก่อนที่คุณจะเริ่มแชร์ไฟล์
แอปของคุณควรทำอย่างไรหากผู้ใช้ไม่ได้เข้าสู่ระบบ วิธีแก้ไขก็คือการส่งคืน
ไม่มีรูทในการใช้งาน queryRoots()
ซึ่งก็คือเคอร์เซอร์รากที่ว่างเปล่าดังนี้
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()
แสดงผล
เคอร์เซอร์ว่างเปล่า ดังที่แสดงด้านบน เพื่อให้มั่นใจว่าเอกสารของผู้ให้บริการ
ใช้งานได้หากผู้ใช้เข้าสู่ระบบผู้ให้บริการ
private fun onLoginButtonClick() {
loginOrLogout()
getContentResolver().notifyChange(
DocumentsContract.buildRootsUri(AUTHORITY),
null
)
}
private void onLoginButtonClick() {
loginOrLogout();
getContentResolver().notifyChange(DocumentsContract
.buildRootsUri(AUTHORITY), null);
}
ดูโค้ดตัวอย่างที่เกี่ยวข้องกับหน้านี้ได้ที่
สำหรับวิดีโอที่เกี่ยวข้องกับหน้านี้ โปรดดูที่
- DevBytes: เฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูลของ Android 4.4: ผู้ให้บริการ
- เฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูล: การสร้าง DocumentsProvider
- ไฟล์เสมือนในเฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูล
ดูข้อมูลที่เกี่ยวข้องเพิ่มเติมได้ที่
- การสร้าง DocumentsProvider
- เปิดไฟล์โดยใช้เฟรมเวิร์กการเข้าถึงพื้นที่เก็บข้อมูล
- ข้อมูลเบื้องต้นเกี่ยวกับผู้ให้บริการเนื้อหา