Membuat penyedia dokumen kustom

Jika Anda mengembangkan aplikasi yang menyediakan layanan penyimpanan untuk file (seperti layanan simpan di {i>cloud<i}), Anda dapat membuat file Anda tersedia melalui Storage Access Framework (SAF) dengan menulis penyedia dokumen kustom. Halaman ini menjelaskan cara membuat penyedia dokumen kustom.

Untuk informasi selengkapnya tentang cara kerja Storage Access Framework, lihat Ringkasan Storage Access Framework.

Manifes

Untuk mengimplementasikan penyedia dokumen kustom, tambahkan kode berikut ke metode manifes:

  • Target API level 19 atau yang lebih tinggi.
  • Elemen <provider> yang mendeklarasikan penyimpanan kustom Anda penyedia layanan.
  • Atribut android:name yang ditetapkan ke nama subclass DocumentsProvider, 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 ditetapkan ke "true". Setelan ini memungkinkan sistem memberikan akses ke aplikasi lain dengan konten di penyedia Anda. Untuk diskusi tentang cara aplikasi lain ini mempertahankan akses mereka ke konten dari penyedia Anda, lihat Persisten izin akses.
  • Izin MANAGE_DOCUMENTS. Secara default, penyedia tersedia oleh semua orang. Menambahkan izin ini akan membatasi penyedia Anda ke sistem. Pembatasan ini penting untuk keamanan.
  • Filter intent yang menyertakan android.content.action.DOCUMENTS_PROVIDER, sehingga penyedia Anda muncul di pemilih 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

Tujuan Intent ACTION_OPEN_DOCUMENT hanya tersedia pada perangkat yang menjalankan Android 4.4 dan yang lebih tinggi. Jika ingin aplikasi Anda mendukung ACTION_GET_CONTENT untuk mengakomodasi perangkat yang menjalankan Android 4.3 dan yang lebih rendah, Anda harus nonaktifkan filter intent ACTION_GET_CONTENT di manifes untuk perangkat yang menjalankan Android 4.4 atau yang lebih tinggi. J penyedia dokumen dan ACTION_GET_CONTENT harus dipertimbangkan saling eksklusif. Jika Anda mendukung keduanya secara bersamaan, aplikasi Anda muncul dua kali di UI pemilih sistem, yang menawarkan dua cara mengakses data yang disimpan. Hal tersebut membingungkan bagi pengguna.

Berikut adalah cara yang disarankan untuk menonaktifkan Filter intent ACTION_GET_CONTENT untuk perangkat menjalankan Android versi 4.4 atau yang lebih tinggi:

  1. Dalam file resource bool.xml di bagian res/values/, tambahkan baris ini:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. Dalam file resource bool.xml di bagian res/values-v19/, tambahkan baris ini:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Tambahkan aktivitas alias untuk menonaktifkan intent ACTION_GET_CONTENT filter untuk versi 4.4 (level API 19) dan yang lebih tinggi. 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 {i>custom<i}, salah satu tugasnya adalah yang menerapkan kelas kontrak, seperti dijelaskan dalam Panduan developer penyedia konten. Kelas kontrak adalah kelas public final yang berisi definisi konstanta untuk URI, nama kolom, jenis MIME, dan {i>metadata<i} lain yang berhubungan dengan penyedia. SAF menyediakan kelas kontrak ini untuk Anda, sehingga Anda tidak perlu menulis sendiri:

Misalnya, berikut ini adalah kolom yang mungkin Anda kembalikan di dalam kursor saat penyedia dokumen Anda dikueri dokumen atau {i>root<i}:

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 yang diperlukan berikut:

Membuat subclass DocumentsProvider

Langkah berikutnya dalam menulis penyedia dokumen kustom adalah membuat subclass class abstrak DocumentsProvider. Setidaknya, Anda harus terapkan metode berikut:

Ini adalah satu-satunya metode yang secara ketat harus Anda implementasikan, tetapi masih banyak lagi yang mungkin Anda inginkan. Lihat DocumentsProvider untuk mengetahui detailnya.

Menentukan root

Implementasi queryRoots() Anda harus menampilkan Cursor yang mengarah ke semua direktori {i>root <i}dari penyedia dokumen Anda, menggunakan kolom yang ditentukan dalam DocumentsContract.Root.

Dalam cuplikan berikut, parameter projection mewakili isian tertentu yang ingin dikembalikan oleh pemanggil. Cuplikan membuat kursor baru dan menambahkan satu baris ke sana—satu {i>root<i}, satu direktori tingkat 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 sebuah 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 rangkaian root dinamis—misalnya, ke USB perangkat yang mungkin terputus atau akun yang dapat digunakan pengguna untuk logout—Anda dapat memperbarui UI dokumen agar tetap sinkron dengan perubahan tersebut menggunakan 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 Anda atas queryChildDocuments() harus mengembalikan Cursor yang mengarah ke semua file di 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 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 di 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 Anda atas queryDocument() harus menampilkan Cursor yang mengarah ke file yang ditentukan, menggunakan kolom yang ditentukan dalam DocumentsContract.Document.

queryDocument() mengembalikan informasi yang sama yang diteruskan pada 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 DocumentsProvider.openDocumentThumbnail(), lalu menambahkan metode FLAG_SUPPORTS_THUMBNAIL penanda 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 ukuran yang ditentukan oleh parameter sizeHint.

Membuka dokumen

Anda harus mengimplementasikan openDocument() untuk menampilkan ParcelFileDescriptor yang mewakili file yang telah ditentukan. Aplikasi lain dapat menggunakan ParcelFileDescriptor yang ditampilkan untuk mengalirkan data. Sistem memanggil metode ini setelah pengguna memilih file, dan aplikasi klien meminta akses 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 masalah struktur data, pertimbangkan untuk menerapkan createReliablePipe() atau Metode createReliableSocketPair(). Metode tersebut memungkinkan Anda untuk 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 saja diubah di bawah root penyedia dokumen dengan mengganti Metode queryRecentDocuments() dan kembali FLAG_SUPPORTS_RECENTS, Cuplikan kode berikut menunjukkan contoh cara menerapkan 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 bisa mendapatkan kode lengkap untuk cuplikan di atas dengan mendownload program StorageProvider contoh kode.

Mendukung pembuatan dokumen

Anda dapat mengizinkan aplikasi klien membuat file dalam penyedia dokumen Anda. Jika aplikasi klien mengirim ACTION_CREATE_DOCUMENT , penyedia dokumen Anda bisa mengizinkan aplikasi klien itu untuk membuat dokumen baru dalam penyedia dokumen.

Untuk mendukung pembuatan dokumen, root Anda harus memiliki FLAG_SUPPORTS_CREATE. Direktori yang memungkinkan pembuatan file baru di dalamnya harus memiliki FLAG_DIR_SUPPORTS_CREATE penanda.

Penyedia dokumen Anda juga perlu menerapkan Metode createDocument(). Saat pengguna memilih direktori dalam penyedia dokumen untuk menyimpan file baru, penyedia dokumen menerima panggilan ke createDocument(). Di dalam implementasi createDocument(), Anda menampilkan COLUMN_DOCUMENT_ID untuk . Aplikasi klien kemudian dapat menggunakan ID tersebut untuk mendapatkan handle untuk file tersebut 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 bisa mendapatkan kode lengkap untuk cuplikan di atas dengan mendownload program StorageProvider contoh kode.

Mendukung fitur pengelolaan dokumen

Selain membuka, membuat, dan melihat file, penyedia dokumen Anda juga dapat membuat aplikasi klien bisa mengganti nama, menyalin, memindahkan, dan menghapus . Untuk menambahkan fungsi pengelolaan dokumen ke penyedia dokumen Anda, tambahkan penanda ke COLUMN_FLAGS kolom untuk menunjukkan fungsi yang didukung. Anda juga perlu menerapkan metode DocumentsProvider yang sesuai .

Tabel berikut memberikan informasi COLUMN_FLAGS tanda dan metode DocumentsProvider yang mendokumentasikan yang perlu diterapkan oleh penyedia layanan untuk mengekspos 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 untuk memberikan akses lihat ke file yang tidak memiliki representasi bytecode langsung. Untuk memungkinkan aplikasi lain melihat file virtual, penyedia dokumen Anda harus membuat file alternatif yang dapat dibuka untuk {i>file<i} virtual.

Misalnya, bayangkan sebuah penyedia dokumen berisi sebuah file format yang tidak dapat dibuka oleh aplikasi lain secara langsung, pada dasarnya file virtual. Saat aplikasi klien mengirim intent ACTION_VIEW tanpa kategori CATEGORY_OPENABLE, maka pengguna dapat memilih {i>file-file<i} virtual ini dalam penyedia dokumen untuk dilihat. Penyedia dokumen kemudian menampilkan file virtual dalam format {i>file<i} yang berbeda, tetapi dapat dibuka, seperti gambar. Aplikasi klien lalu dapat membuka file virtual untuk dilihat pengguna.

Untuk mendeklarasikan bahwa dokumen dalam penyedia adalah virtual, Anda perlu menambahkan FLAG_VIRTUAL_DOCUMENT penanda ke file yang dikembalikan oleh queryDocument() . Tanda ini memberi tahu aplikasi klien bahwa file tidak memiliki dan tidak bisa langsung dibuka.

Jika Anda menyatakan bahwa file di penyedia dokumen Anda adalah virtual, sangat disarankan agar Anda menyediakannya di Jenis MIME seperti gambar atau PDF. Penyedia dokumen mendeklarasikan jenis MIME alternatif yang mendukung tampilan file virtual dengan mengganti getDocumentStreamTypes() . Saat aplikasi klien memanggil getStreamTypes(android.net.Uri, java.lang.String) , sistem akan memanggil metode getDocumentStreamTypes() penyedia dokumen. Tujuan getDocumentStreamTypes() lalu mengembalikan satu larik tipe MIME alternatif yang yang didukung oleh penyedia dokumen untuk file tersebut.

Setelah klien menentukan bahwa penyedia dokumen dapat menghasilkan dokumen dalam file yang dapat dilihat , aplikasi klien memanggil openTypedAssetFileDescriptor() , yang secara internal memanggil metode openTypedDocument() . Penyedia dokumen mengembalikan file ke aplikasi klien dalam format file yang diminta.

Cuplikan kode berikut menunjukkan implementasi sederhana dari getDocumentStreamTypes() dan openTypedDocument() metode.

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&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();
}

Keamanan

Misalkan penyedia dokumen Anda adalah layanan penyimpanan cloud yang dilindungi sandi dan Anda ingin memastikan bahwa pengguna telah {i>login<i} sebelum Anda mulai berbagi file mereka. Apa yang harus dilakukan aplikasi Anda jika pengguna tidak login? Solusinya adalah mengembalikan nol root 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 melakukan kueri root dari root penyedia dokumen setiap kali status login pengguna berubah. Jika pengguna tidak login, panggilan ke queryRoots() akan menampilkan kursor kosong, seperti yang ditampilkan di atas. Hal 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:

Untuk informasi terkait lainnya, lihat: