Özel doküman sağlayıcı oluşturma

Buluta kaydetme hizmeti gibi dosyalar için depolama hizmetleri sağlayan bir uygulama geliştiriyorsanız özel bir belge sağlayıcı yazarak dosyalarınızı Depolama Erişim Çerçevesi (SAF) üzerinden kullanıma sunabilirsiniz. Bu sayfada, özel doküman sağlayıcının nasıl oluşturulacağı açıklanmaktadır.

Depolama Erişim Çerçevesi'nin işleyiş şekli hakkında daha fazla bilgi için Depolama Erişim Çerçevesi'ne genel bakış sayfasını inceleyin.

Manifest

Özel bir belge sağlayıcı uygulamak için uygulamanızın manifest dosyasına aşağıdakileri ekleyin:

  • API düzeyi 19 veya üstü bir hedef.
  • Özel depolama alanı sağlayıcınızı tanımlayan bir <provider> öğesi.
  • android:name özelliği, DocumentsProvider alt sınıfınızın adına ayarlandı. Bu alt sınıf, paket adı da dahil olmak üzere bu alt sınıfın sınıf adıdır:

    com.example.android.storageprovider.MyCloudProvider.

  • Paketinizin adı (bu örnekte com.example.android.storageprovider) ve içerik sağlayıcının türü olan android:authority özelliği (documents).
  • android:exported özelliği "true" olarak ayarlandı. Diğer uygulamaların görebilmesi için sağlayıcınızı dışa aktarmanız gerekir.
  • android:grantUriPermissions özelliği "true" olarak ayarlandı. Bu ayar, sistemin, diğer uygulamaların sağlayıcınızdaki içeriğe erişmesine izin vermesine olanak tanır. Bu diğer uygulamaların, sağlayıcınızdan gelen içeriğe erişmeye nasıl devam edebileceğini öğrenmek için İzinleri sürdürme bölümüne bakın.
  • MANAGE_DOCUMENTS izni. Varsayılan olarak bir sağlayıcı herkes tarafından kullanılabilir. Bu iznin eklenmesi sağlayıcınızı sistemle kısıtlar. Bu kısıtlama, güvenlik açısından önemlidir.
  • Sistem sağlayıcı aradığında sağlayıcınızın seçicide görünmesini sağlayan, android.content.action.DOCUMENTS_PROVIDER işlemini içeren bir amaç filtresi.

Bir sağlayıcı içeren örnek bir manifestten alıntıları burada bulabilirsiniz:

<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 ve önceki sürümleri çalıştıran cihazları destekleme

ACTION_OPEN_DOCUMENT amacı yalnızca Android 4.4 ve sonraki sürümleri çalıştıran cihazlarda kullanılabilir. Uygulamanızın, Android 4.3 ve önceki sürümleri çalıştıran cihazları barındırmak için ACTION_GET_CONTENT desteklemesini istiyorsanız Android 4.4 veya sonraki sürümleri çalıştıran cihazlar için manifest dosyanızda ACTION_GET_CONTENT intent filtresini devre dışı bırakmanız gerekir. Bir belge sağlayıcı ile ACTION_GET_CONTENT karşılıklı olarak birbirini dışlamalıdır. İkisini de aynı anda destekliyorsanız uygulamanız sistem seçici kullanıcı arayüzünde iki kez görünür ve depolanan verilerinize iki farklı şekilde erişme imkanı sunar. Bu durum, kullanıcıların kafasını karıştırabilir.

Android 4.4 veya sonraki sürümleri çalıştıran cihazlarda ACTION_GET_CONTENT intent filtresini devre dışı bırakmak için önerilen yöntem aşağıda açıklanmıştır:

  1. res/values/ altındaki bool.xml kaynakları dosyanıza şu satırı ekleyin:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. res/values-v19/ altındaki bool.xml kaynakları dosyanıza şu satırı ekleyin:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. 4.4 (API düzeyi 19) ve sonraki sürümler için ACTION_GET_CONTENT amaç filtresini devre dışı bırakmak üzere bir etkinlik takma adı ekleyin. Örneğin:
    <!-- 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>
    

Sözleşmeler

Özel bir içerik sağlayıcı yazdığınızda genellikle görevlerden biri, İçerik sağlayıcılar geliştirici kılavuzunda açıklandığı gibi sözleşme sınıflarını uygulamaktır. Sözleşme sınıfı URI'ler, sütun adları, MIME türleri ve sağlayıcıyla ilgili diğer meta veriler için sabit tanımlar içeren bir public final sınıfıdır. SAF, sizin için aşağıdaki sözleşme sınıflarını sağlar. Bu nedenle kendi bilgilerinizi yazmanıza gerek yoktur:

Örneğin, belge sağlayıcınız belge veya kök sorgulandığında imleç içinde döndürebileceğiniz sütunlar şunlardır:

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,};

Kök imlecinin, gerekli bazı sütunları içermesi gerekir. Bu sütunlar şunlardır:

Dokümanlara ilişkin imlecin aşağıdaki gerekli sütunları içermesi gerekir:

DocumentsProvider alt sınıfı oluşturma

Özel belge sağlayıcı yazmanın bir sonraki adımı DocumentsProvider soyut sınıfını alt sınıflandırmaktır. En azından aşağıdaki yöntemleri uygulamanız gerekir:

Bunlar kesinlikle uygulamanız gereken yegâne yöntemlerdir, ancak uygulamak isteyebileceğiniz daha pek çok yöntem vardır. Ayrıntılar için DocumentsProvider sayfasını ziyaret edin.

Bir kök tanımlayın

queryRoots() uygulamanız, DocumentsContract.Root içinde tanımlanan sütunları kullanarak belge sağlayıcınızın tüm kök dizinlerine işaret eden bir Cursor döndürmelidir.

Aşağıdaki snippet'te projection parametresi, çağrıyı yapanın geri almak istediği belirli alanları temsil eder. Snippet yeni bir imleç oluşturur ve ona bir satır ekler: Bir kök dizin ya da İndirilenler veya Görseller gibi üst düzey bir dizin. Çoğu sağlayıcının yalnızca bir kökü vardır. Birden fazla hesabınız olabilir. Örneğin, birden fazla kullanıcı hesabı söz konusuysa. Bu durumda, imlece ikinci bir satır eklemeniz yeterlidir.

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

Doküman sağlayıcınız dinamik bir kök grubuna (örneğin, bağlantısı kesilmiş bir USB cihaza veya kullanıcının oturumunu kapatabileceği bir hesaba) bağlanırsa doküman kullanıcı arayüzünü, aşağıdaki kod snippet'inde gösterildiği gibi ContentResolver.notifyChange() yöntemini kullanarak bu değişikliklerle senkronize olacak şekilde güncelleyebilirsiniz.

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

Sağlayıcıdaki dokümanları listeleme

queryChildDocuments() uygulamanız, DocumentsContract.Document içinde tanımlanan sütunları kullanarak belirtilen dizindeki tüm dosyaları işaret eden bir Cursor döndürmelidir.

Bu yöntem, kullanıcı seçici kullanıcı arayüzünde kök yapınızı seçtiğinde çağrılır. Bu yöntem, COLUMN_DOCUMENT_ID tarafından belirtilen belge kimliğinin alt öğelerini alır. Daha sonra kullanıcı, belge sağlayıcınızın içinden bir alt dizin seçtiğinde sistem bu yöntemi çağırır.

Bu snippet, istenen sütunlarla yeni bir imleç oluşturur, ardından üst dizindeki her bir alt öğe hakkında imlece bilgi ekler. Alt öğe bir resim, başka bir dizin ve herhangi bir dosya olabilir.

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

Belge bilgilerini alma

queryDocument() uygulamanız, DocumentsContract.Document içinde tanımlanan sütunlar kullanılarak belirtilen dosyaya işaret eden bir Cursor döndürmelidir.

queryDocument() yöntemi, queryChildDocuments() içinde iletilen bilgilerin aynısını döndürür, ancak belirli bir dosya için:

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

Belge sağlayıcınız, DocumentsProvider.openDocumentThumbnail() yöntemini geçersiz kılarak ve desteklenen dosyalara FLAG_SUPPORTS_THUMBNAIL işaretini ekleyerek belge için küçük resimler de sağlayabilir. Aşağıdaki kod snippet'i, DocumentsProvider.openDocumentThumbnail() öğesinin nasıl uygulanacağına dair bir örnek sunar.

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

Dikkat: Bir belge sağlayıcı, sizeHint parametresiyle belirtilen boyutun iki katından daha fazla küçük resim görüntüsü döndürmemelidir.

Doküman açın

Belirtilen dosyayı temsil eden bir ParcelFileDescriptor döndürmek için openDocument() uygulamanız gerekir. Diğer uygulamalar, veri akışı için döndürülen ParcelFileDescriptor özelliğini kullanabilir. Kullanıcı bir dosya seçtikten sonra sistem bu yöntemi çağırır ve istemci uygulaması openFileDescriptor() yöntemini çağırarak dosyaya erişim isteğinde bulunur. Örneğin:

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

Belge sağlayıcınız dosya akışı sağlıyorsa veya karmaşık veri yapılarını işliyorsa createReliablePipe() veya createReliableSocketPair() yöntemlerini uygulamayı düşünün. Bu yöntemler, ParcelFileDescriptor nesne çifti oluşturmanıza olanak tanır. Böylece birini döndürüp diğerini ParcelFileDescriptor.AutoCloseOutputStream veya ParcelFileDescriptor.AutoCloseInputStream aracılığıyla gönderebilirsiniz.

Son dokümanları ve aramayı destekle

queryRecentDocuments() yöntemini geçersiz kılarak ve FLAG_SUPPORTS_RECENTS değerini döndürerek, belge sağlayıcınızın kök dizininde son değiştirilen dokümanların listesini sağlayabilirsiniz. Aşağıdaki kod snippet'inde queryRecentDocuments() yöntemlerinin nasıl uygulanacağına dair bir örnek gösterilmektedir.

Kotlin

override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor {
    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    val result = MatrixCursor(resolveDocumentProjection(projection))

    val parent: File = getFileForDocId(rootId)

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    val lastModifiedFiles = PriorityQueue(
            5,
            Comparator<File> { i, j ->
                Long.compare(i.lastModified(), j.lastModified())
            }
    )

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    val pending : MutableList<File> = mutableListOf()

    // Start by adding the parent to the list of files to be processed
    pending.add(parent)

    // Do while we still have unexamined files
    while (pending.isNotEmpty()) {
        // Take a file from the list of unprocessed files
        val file: File = pending.removeAt(0)
        if (file.isDirectory) {
            // If it's a directory, add all its children to the unprocessed list
            pending += file.listFiles()
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file)
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) {
        val file: File = lastModifiedFiles.remove()
        includeFile(result, null, file)
    }
    return result
}

Java

@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
        throws FileNotFoundException {

    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result =
        new MatrixCursor(resolveDocumentProjection(projection));

    final File parent = getFileForDocId(rootId);

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    PriorityQueue lastModifiedFiles =
        new PriorityQueue(5, new Comparator() {

        public int compare(File i, File j) {
            return Long.compare(i.lastModified(), j.lastModified());
        }
    });

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    final LinkedList pending = new LinkedList();

    // Start by adding the parent to the list of files to be processed
    pending.add(parent);

    // Do while we still have unexamined files
    while (!pending.isEmpty()) {
        // Take a file from the list of unprocessed files
        final File file = pending.removeFirst();
        if (file.isDirectory()) {
            // If it's a directory, add all its children to the unprocessed list
            Collections.addAll(pending, file.listFiles());
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file);
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) {
        final File file = lastModifiedFiles.remove();
        includeFile(result, null, file);
    }
    return result;
}

StorageProvider kod örneğini indirerek yukarıdaki snippet'in tam kodunu edinebilirsiniz.

Doküman oluşturma işlemini destekleyin

İstemci uygulamalarının, doküman sağlayıcınızın içinde dosya oluşturmasına izin verebilirsiniz. Bir istemci uygulaması ACTION_CREATE_DOCUMENT amacı gönderirse belge sağlayıcınız, ilgili istemci uygulamasının belge sağlayıcı içerisinde yeni dokümanlar oluşturmasına izin verebilir.

Belge oluşturmayı desteklemek için kök dosyanızda FLAG_SUPPORTS_CREATE işareti bulunmalıdır. İçlerinde yeni dosyaların oluşturulmasına izin veren dizinler FLAG_DIR_SUPPORTS_CREATE işaretinin olması gerekir.

Belge sağlayıcınızın da createDocument() yöntemini uygulaması gerekir. Bir kullanıcı, yeni dosya kaydetmek için doküman sağlayıcınızın içinden bir dizin seçtiğinde doküman sağlayıcıya createDocument() çağrısı gönderilir. createDocument() yönteminin uygulanması sonucunda, dosya için yeni bir COLUMN_DOCUMENT_ID döndürüyorsunuz. Daha sonra istemci uygulaması, bu kimliği kullanarak dosya için bir herkese açık kullanıcı adı alabilir ve sonuç olarak, yeni dosyaya yazması için openDocument() yöntemini çağırabilir.

Aşağıdaki kod snippet'inde, belge sağlayıcıda yeni bir dosyanın nasıl oluşturulacağı gösterilmektedir.

Kotlin

override fun createDocument(documentId: String?, mimeType: String?, displayName: String?): String {
    val parent: File = getFileForDocId(documentId)
    val file: File = try {
        File(parent.path, displayName).apply {
            createNewFile()
            setWritable(true)
            setReadable(true)
        }
    } catch (e: IOException) {
        throw FileNotFoundException(
                "Failed to create document with name $displayName and documentId $documentId"
        )
    }

    return getDocIdForFile(file)
}

Java

@Override
public String createDocument(String documentId, String mimeType, String displayName)
        throws FileNotFoundException {

    File parent = getFileForDocId(documentId);
    File file = new File(parent.getPath(), displayName);
    try {
        file.createNewFile();
        file.setWritable(true);
        file.setReadable(true);
    } catch (IOException e) {
        throw new FileNotFoundException("Failed to create document with name " +
                displayName +" and documentId " + documentId);
    }
    return getDocIdForFile(file);
}

StorageProvider kod örneğini indirerek yukarıdaki snippet'in tam kodunu edinebilirsiniz.

Doküman yönetimi özelliklerini destekleme

Belge sağlayıcınız; dosya açma, oluşturma ve görüntülemenin yanı sıra istemci uygulamalarına dosyaları yeniden adlandırma, kopyalama, taşıma ve silme izni de verebilir. Doküman sağlayıcınıza doküman yönetimi işlevi eklemek için dokümanın COLUMN_FLAGS sütununa, desteklenen işlevi belirten bir işaret ekleyin. Ayrıca, DocumentsProvider sınıfının ilgili yöntemini de uygulamanız gerekir.

Aşağıdaki tabloda, belge sağlayıcının belirli özellikleri açığa çıkarmak için uygulaması gereken COLUMN_FLAGS işareti ve DocumentsProvider yöntemi gösterilmektedir.

Öne Çıkarın İşaretle Yöntem
Dosya silme FLAG_SUPPORTS_DELETE deleteDocument()
Dosyaları yeniden adlandırma FLAG_SUPPORTS_RENAME renameDocument()
Bir dosyayı doküman sağlayıcıdaki yeni bir üst dizine kopyalama FLAG_SUPPORTS_COPY copyDocument()
Doküman sağlayıcıda bir dizinden diğerine dosya taşıma FLAG_SUPPORTS_MOVE moveDocument()
Bir dosyayı üst dizininden kaldırma FLAG_SUPPORTS_REMOVE removeDocument()

Sanal dosyaları ve alternatif dosya biçimlerini destekleme

Android 7.0'da (API düzeyi 24) kullanıma sunulan bir özellik olan sanal dosyalar, belge sağlayıcıların doğrudan bayt kodu temsili olmayan dosyalara görüntüleme erişimi sağlamalarına olanak tanır. Diğer uygulamaların sanal dosyaları görüntüleyebilmesi için belge sağlayıcınızın, sanal dosyalar için alternatif olarak açılabilir bir dosya temsili oluşturması gerekir.

Örneğin, bir belge sağlayıcının, diğer uygulamaların doğrudan açamadığı bir dosya biçimi (yani sanal bir dosya) içerdiğini düşünün. Bir istemci uygulaması, CATEGORY_OPENABLE kategorisi olmadan bir ACTION_VIEW amacı gönderdiğinde kullanıcılar görüntüleme için belge sağlayıcıda bu sanal dosyaları seçebilir. Ardından belge sağlayıcı, sanal dosyayı resim gibi farklı ancak açılabilir bir dosya biçiminde döndürür. Böylece istemci uygulaması, kullanıcının görüntülemesi için sanal dosyayı açabilir.

Sağlayıcıdaki bir dokümanın sanal olduğunu beyan etmek için queryDocument() yöntemiyle döndürülen dosyaya FLAG_VIRTUAL_DOCUMENT işaretini eklemeniz gerekir. Bu işaret, istemci uygulamalarına dosyanın doğrudan bayt kodu temsilinin olmadığı ve doğrudan açılamadığı konusunda uyarı verir.

Doküman sağlayıcınızdaki bir dosyanın sanal olduğunu belirtirseniz bu dosyayı resim veya PDF gibi başka bir MIME türünde kullanılabilir hale getirmeniz kesinlikle önerilir. Belge sağlayıcı, sanal dosyayı görüntülemek için desteklediği alternatif MIME türlerini getDocumentStreamTypes() yöntemini geçersiz kılarak tanımlar. İstemci uygulamaları getStreamTypes(android.net.Uri, java.lang.String) yöntemini çağırdığında sistem, belge sağlayıcının getDocumentStreamTypes() yöntemini çağırır. Daha sonra getDocumentStreamTypes() yöntemi, belge sağlayıcının dosya için desteklediği alternatif MIME türleri dizisi döndürür.

İstemci, belge sağlayıcının belgeyi görüntülenebilir bir dosya biçiminde oluşturabileceğini belirledikten sonra, istemci uygulaması openTypedAssetFileDescriptor() yöntemini çağırır. Bu yöntem, belge sağlayıcının openTypedDocument() yöntemini dahili olarak çağırır. Belge sağlayıcı, dosyayı istemci uygulamasına istenen dosya biçiminde döndürür.

Aşağıdaki kod snippet'inde getDocumentStreamTypes() ve openTypedDocument() yöntemlerinin basit bir şekilde uygulanması gösterilmektedir.

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

Güvenlik

Belge sağlayıcınızın şifre korumalı bir bulut depolama hizmeti olduğunu ve siz dosyalarını paylaşmaya başlamadan önce kullanıcıların giriş yaptığından emin olmak istediğinizi varsayalım. Kullanıcı giriş yapmamışsa uygulamanız ne yapmalıdır? Çözüm, queryRoots() uygulamanızda sıfır kök döndürmektir. Yani boş bir kök imleç:

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

Diğer adım, getContentResolver().notifyChange() yöntemini çağırmaktır. DocumentsContract etkinliğini hatırlıyor musunuz? URI'yı yapmak için kullanıyoruz. Aşağıdaki snippet, kullanıcının giriş durumu her değiştiğinde sisteme belge sağlayıcınızın köklerini sorgulamasını söyler. Kullanıcı giriş yapmamışsa queryRoots() çağrısı, yukarıda gösterildiği gibi boş bir imleç döndürür. Bu şekilde, sağlayıcının belgeleri yalnızca, kullanıcı sağlayıcıya giriş yapmışsa kullanılabilir.

Kotlin

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

Java

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

Bu sayfayla ilgili örnek kod için şuraya bakın:

Bu sayfayla ilgili videolar için aşağıdaki kaynaklara göz atın:

İlgili daha fazla bilgi için aşağıdaki konulara bakın: