Jika Anda mengembangkan aplikasi yang menyediakan layanan penyimpanan untuk file (seperti layanan simpan di cloud), Anda dapat menyediakan file melalui Storage Access Framework (SAF) dengan menulis penyedia dokumen kustom. Halaman ini menjelaskan cara membuat penyedia dokumen kustom.
Untuk mengetahui informasi selengkapnya tentang cara kerja Storage Access Framework, lihat ringkasan Storage Access Framework.
Manifes
Untuk menerapkan penyedia dokumen kustom, tambahkan kode berikut ke manifes aplikasi Anda:
- Target API level 19 atau yang lebih tinggi.
- Elemen
<provider>
yang mendeklarasikan penyedia penyimpanan kustom Anda. -
Atribut
android:name
yang ditetapkan ke nama subclassDocumentsProvider
Anda, yang merupakan nama class-nya, termasuk nama paket:com.example.android.storageprovider.MyCloudProvider
. -
Atribut
android:authority
, yang merupakan nama paket Anda (dalam contoh ini,com.example.android.storageprovider
) ditambah jenis penyedia konten (documents
). - Atribut
android:exported
yang ditetapkan ke"true"
. Anda harus mengekspor penyedia Anda agar aplikasi lain dapat melihatnya. - Atribut
android:grantUriPermissions
yang ditetapkan ke"true"
. Setelan ini memungkinkan sistem memberi aplikasi lain akses ke konten di penyedia Anda. Untuk mengetahui diskusi tentang cara aplikasi lain tersebut dapat mempertahankan akses mereka ke konten dari penyedia Anda, lihat Mempertahankan izin. - Izin
MANAGE_DOCUMENTS
. Secara default, penyedia tersedia untuk semua orang. Menambahkan izin ini akan membatasi penyedia Anda ke sistem. Pembatasan ini penting untuk keamanan. - Filter intent yang menyertakan
tindakan
android.content.action.DOCUMENTS_PROVIDER
, sehingga penyedia Anda muncul di alat pilih saat sistem menelusuri penyedia.
Berikut ini adalah kutipan dari manifes contoh yang menyertakan penyedia:
<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>
Perangkat pendukung yang menjalankan Android 4.3 dan yang lebih rendah
Intent
ACTION_OPEN_DOCUMENT
hanya tersedia
di perangkat yang menjalankan Android 4.4 dan yang lebih tinggi.
Jika ingin aplikasi Anda mendukung ACTION_GET_CONTENT
guna mengakomodasi perangkat yang menjalankan Android 4.3 dan yang lebih rendah, Anda harus
menonaktifkan filter intent ACTION_GET_CONTENT
dalam
manifes untuk perangkat yang menjalankan Android 4.4 atau yang lebih tinggi. Penyedia dokumen dan ACTION_GET_CONTENT
harus dianggap eksklusif satu sama lain. Jika Anda mendukung keduanya secara bersamaan, aplikasi Anda
akan muncul dua kali di UI alat pilih sistem, yang menawarkan dua cara berbeda untuk mengakses
data yang tersimpan. Hal tersebut membingungkan bagi pengguna.
Berikut adalah cara yang direkomendasikan untuk menonaktifkan
filter intent ACTION_GET_CONTENT
untuk perangkat
yang menjalankan Android versi 4.4 atau yang lebih tinggi:
- Dalam file resource
bool.xml
di bawahres/values/
, tambahkan baris ini:<bool name="atMostJellyBeanMR2">true</bool>
- Dalam file resource
bool.xml
di bawahres/values-v19/
, tambahkan baris ini:<bool name="atMostJellyBeanMR2">false</bool>
- Tambahkan
alias
aktivitas guna menonaktifkan filter intent
ACTION_GET_CONTENT
untuk versi 4.4 (API level 19) dan yang lebih baru. Contoh:<!-- 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>
Kontrak
Biasanya saat Anda menulis penyedia konten kustom, salah satu tugasnya adalah
mengimplementasikan class kontrak, seperti yang dijelaskan dalam
panduan developer
Penyedia konten. Class kontrak adalah class public final
yang berisi definisi konstanta untuk URI, nama kolom, jenis MIME, dan
metadata lain yang berkaitan dengan penyedia. SAF
menyediakan class kontrak ini untuk Anda, jadi Anda tidak perlu menulis
sendiri:
Misalnya, berikut adalah kolom yang mungkin Anda tampilkan di kursor saat penyedia dokumen Anda membuat kueri dokumen atau root:
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,};
Kursor untuk root perlu menyertakan kolom yang dibutuhkan. Kolom tersebut meliputi:
Kursor untuk dokumen harus menyertakan kolom-kolom wajib berikut:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
Membuat subclass DocumentsProvider
Langkah berikutnya dalam menulis penyedia dokumen kustom adalah membuat subclass untuk DocumentsProvider
class abstrak. Setidaknya, Anda harus menerapkan metode berikut:
Metode tersebut adalah satu-satunya metode yang harus Anda terapkan, tetapi masih
banyak lagi yang mungkin ingin Anda terapkan. Untuk mengetahui detailnya, lihat DocumentsProvider
.
Menentukan root
Implementasi queryRoots()
harus menampilkan Cursor
yang mengarah ke semua direktori utama penyedia dokumen Anda, menggunakan kolom yang ditentukan dalam DocumentsContract.Root
.
Dalam cuplikan berikut, parameter projection
mewakili
kolom tertentu yang ingin didapatkan kembali oleh pemanggil. Cuplikan membuat kursor baru
dan menambahkan satu baris ke dalamnya—satu root, satu direktori level atas, seperti
Download atau Gambar. Sebagian besar penyedia hanya memiliki satu root. Anda mungkin memiliki lebih dari satu,
misalnya, dalam kasus beberapa akun pengguna. Dalam hal ini, cukup tambahkan
baris kedua ke kursor.
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; }
Jika penyedia dokumen Anda terhubung ke kumpulan root dinamis—misalnya, ke perangkat USB yang mungkin terputus atau akun yang dapat digunakan pengguna untuk logout—Anda dapat mengupdate UI dokumen agar tetap sinkron dengan perubahan tersebut menggunakan metode ContentResolver.notifyChange()
, seperti yang ditunjukkan dalam cuplikan kode berikut.
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);
Mencantumkan dokumen dalam penyedia
Implementasi
queryChildDocuments()
harus menampilkan Cursor
yang mengarah ke semua file dalam
direktori yang ditentukan, menggunakan kolom yang ditentukan dalam
DocumentsContract.Document
.
Metode ini dipanggil saat pengguna memilih root Anda di UI alat pilih.
Metode ini mengambil turunan dari ID dokumen yang ditentukan oleh COLUMN_DOCUMENT_ID
.
Sistem kemudian memanggil metode ini setiap kali pengguna memilih subdirektori dalam penyedia dokumen Anda.
Cuplikan ini membuat kursor baru dengan kolom yang diminta, lalu menambahkan informasi tentang setiap turunan langsung pada direktori induk ke kursor. Turunan dapat berupa gambar, direktori lain — file apa pun:
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; }
Mendapatkan informasi dokumen
Implementasi
queryDocument()
harus menampilkan Cursor
yang mengarah ke file yang ditentukan,
menggunakan kolom yang ditentukan dalam DocumentsContract.Document
.
Metode queryDocument()
menampilkan informasi yang sama yang diteruskan dalam queryChildDocuments()
, tetapi untuk file tertentu:
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; }
Penyedia dokumen Anda juga dapat menyediakan thumbnail untuk dokumen dengan mengganti metode DocumentsProvider.openDocumentThumbnail()
dan menambahkan flag FLAG_SUPPORTS_THUMBNAIL
ke file yang didukung.
Cuplikan kode berikut memberikan contoh cara mengimplementasikan
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); }
Perhatian: Penyedia dokumen tidak boleh menampilkan gambar thumbnail lebih dari dua kali lipat ukuran yang ditentukan oleh parameter sizeHint
.
Membuka dokumen
Anda harus mengimplementasikan openDocument()
untuk menampilkan ParcelFileDescriptor
yang mewakili
file yang ditentukan. Aplikasi lain dapat menggunakan ParcelFileDescriptor
yang ditampilkan
untuk melakukan streaming data. Sistem memanggil metode ini setelah pengguna memilih file,
dan aplikasi klien meminta akses ke file tersebut dengan memanggil
openFileDescriptor()
.
Contoh:
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); } }
Jika penyedia dokumen Anda melakukan streaming file atau menangani struktur data yang rumit, sebaiknya implementasikan metode createReliablePipe()
atau createReliableSocketPair()
.
Metode tersebut memungkinkan Anda membuat sepasang
objek ParcelFileDescriptor
, tempat Anda dapat menampilkan salah satunya
dan mengirim yang lainnya melalui
ParcelFileDescriptor.AutoCloseOutputStream
atau
ParcelFileDescriptor.AutoCloseInputStream
.
Mendukung dokumen dan penelusuran terbaru
Anda dapat memberikan daftar dokumen yang baru diubah di bawah root penyedia dokumen Anda dengan mengganti metode queryRecentDocuments()
dan menampilkan FLAG_SUPPORTS_RECENTS
. Cuplikan kode berikut menunjukkan contoh cara mengimplementasikan metode 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; }
Anda dapat memperoleh kode lengkap untuk cuplikan di atas dengan mendownload contoh kode StorageProvider.
Mendukung pembuatan dokumen
Anda dapat mengizinkan aplikasi klien membuat file dalam penyedia dokumen Anda.
Jika aplikasi klien mengirim intent ACTION_CREATE_DOCUMENT
, penyedia dokumen Anda dapat mengizinkan aplikasi klien tersebut untuk membuat dokumen baru dalam penyedia dokumen.
Untuk mendukung pembuatan dokumen, root Anda harus memiliki flag FLAG_SUPPORTS_CREATE
.
Direktori yang memungkinkan pembuatan file baru di dalamnya harus memiliki flag FLAG_DIR_SUPPORTS_CREATE
.
Penyedia dokumen Anda juga harus mengimplementasikan metode createDocument()
. Saat pengguna memilih direktori dalam penyedia dokumen Anda untuk menyimpan file baru, penyedia dokumen akan menerima panggilan ke createDocument()
. Di dalam implementasi metode
createDocument()
, tampilkan
COLUMN_DOCUMENT_ID
baru untuk
file tersebut. Selanjutnya, aplikasi klien dapat menggunakan ID tersebut untuk mendapatkan handle bagi file dan, pada akhirnya, memanggil openDocument()
untuk menulis ke file baru.
Cuplikan kode berikut menunjukkan cara membuat file baru dalam penyedia dokumen.
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); }
Anda dapat memperoleh kode lengkap untuk cuplikan di atas dengan mendownload contoh kode StorageProvider.
Mendukung fitur pengelolaan dokumen
Selain membuka, membuat, dan melihat file, penyedia dokumen Anda juga dapat memberi aplikasi klien kemampuan untuk mengganti nama, menyalin, memindahkan, dan menghapus file. Untuk menambahkan fungsionalitas pengelolaan dokumen ke penyedia dokumen Anda, tambahkan flag ke kolom COLUMN_FLAGS
dokumen untuk menunjukkan fungsi yang didukung. Anda juga harus menerapkan
metode class DocumentsProvider
yang sesuai.
Tabel berikut menyediakan flag COLUMN_FLAGS
dan metode DocumentsProvider
yang perlu diimplementasikan oleh penyedia dokumen untuk menampilkan fitur tertentu.
Fitur | Flag | Metode |
---|---|---|
Menghapus file |
FLAG_SUPPORTS_DELETE
|
deleteDocument()
|
Mengganti nama file |
FLAG_SUPPORTS_RENAME
|
renameDocument()
|
Menyalin file ke direktori induk baru dalam penyedia dokumen |
FLAG_SUPPORTS_COPY
|
copyDocument()
|
Memindahkan file dari satu direktori ke direktori lain dalam penyedia dokumen |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
Menghapus file dari direktori induknya |
FLAG_SUPPORTS_REMOVE
|
removeDocument()
|
Mendukung file virtual dan format file alternatif
File virtual, fitur yang diperkenalkan di Android 7.0 (API level 24), memungkinkan penyedia dokumen memberikan akses melihat ke file yang tidak memiliki representasi bytecode langsung. Agar aplikasi lain dapat melihat file virtual, penyedia dokumen Anda harus menghasilkan representasi file alternatif yang dapat dibuka untuk file virtual.
Misalnya, bayangkan penyedia dokumen berisi format file yang tidak dapat dibuka secara langsung oleh aplikasi lain, pada dasarnya adalah file virtual.
Saat aplikasi klien mengirimkan intent ACTION_VIEW
tanpa kategori CATEGORY_OPENABLE
, pengguna dapat memilih file virtual ini dalam penyedia dokumen untuk dilihat. Penyedia dokumen kemudian menampilkan file virtual dalam format file yang berbeda, tetapi dapat dibuka, seperti gambar.
Aplikasi klien lalu dapat membuka file virtual untuk dilihat pengguna.
Untuk mendeklarasikan bahwa dokumen di penyedia adalah virtual, Anda harus menambahkan flag
FLAG_VIRTUAL_DOCUMENT
ke file yang ditampilkan oleh
metode
queryDocument()
. Flag ini memberi tahu aplikasi klien bahwa file tidak memiliki representasi bytecode
langsung dan tidak dapat dibuka secara langsung.
Jika Anda mendeklarasikan bahwa file di penyedia dokumen bersifat virtual, sangat disarankan agar Anda menyediakannya dalam jenis MIME seperti gambar atau PDF. Penyedia dokumen mendeklarasikan jenis MIME alternatif yang didukungnya untuk melihat file virtual dengan mengganti metode getDocumentStreamTypes()
. Saat aplikasi klien memanggil metode getStreamTypes(android.net.Uri, java.lang.String)
, sistem akan memanggil metode getDocumentStreamTypes()
penyedia dokumen. Metode
getDocumentStreamTypes()
kemudian menampilkan array jenis MIME alternatif yang
didukung oleh penyedia dokumen untuk file tersebut.
Setelah klien menentukan bahwa penyedia dokumen dapat menghasilkan dokumen dalam format file yang dapat dilihat, aplikasi klien akan memanggil metode openTypedAssetFileDescriptor()
, yang secara internal memanggil metode openTypedDocument()
penyedia dokumen. Penyedia dokumen menampilkan file ke aplikasi klien dalam
format file yang diminta.
Cuplikan kode berikut menunjukkan implementasi sederhana metode
getDocumentStreamTypes()
dan
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 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(); }
Keamanan
Misalnya penyedia dokumen Anda adalah layanan penyimpanan cloud yang dilindungi sandi dan Anda ingin memastikan pengguna sudah login sebelum mulai berbagi file mereka.
Apa yang harus dilakukan aplikasi Anda jika pengguna tidak login? Solusinya adalah menampilkan
root nol dalam implementasi queryRoots()
. Artinya, kursor root kosong:
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; }
Langkah lainnya adalah memanggil getContentResolver().notifyChange()
.
Ingat DocumentsContract
? Kita menggunakannya untuk membuat
URI ini. Cuplikan berikut memberi tahu sistem untuk mengkueri root penyedia dokumen Anda setiap kali status login pengguna berubah. Jika pengguna tidak
login, panggilan ke queryRoots()
akan menampilkan
kursor kosong, seperti yang ditunjukkan di atas. Cara ini memastikan bahwa dokumen penyedia hanya tersedia jika pengguna login ke penyedia.
Kotlin
private fun onLoginButtonClick() { loginOrLogout() getContentResolver().notifyChange( DocumentsContract.buildRootsUri(AUTHORITY), null ) }
Java
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }
Untuk contoh kode yang terkait dengan halaman ini, lihat:
Untuk video yang terkait dengan halaman ini, lihat:
- DevBytes: Android 4.4 Storage Access Framework: Penyedia
- Storage Access Framework: Membuat DocumentsProvider
- File Virtual dalam Storage Access Framework
Untuk informasi terkait lainnya, lihat:
- Membuat DocumentProvider
- Membuka File menggunakan Storage Access Framework
- Dasar-Dasar Penyedia Konten