ایجاد ارائه‌دهنده سند سفارشی

اگر درحال توسعه برنامه‌ای هستید که خدمات ذخیره‌سازی برای فایل‌ها ارائه می‌دهد (مانند سرویس ذخیره ابری)، می‌توانید فایل‌هایتان را ازطریق «چارچوب دسترسی به فضای ذخیره‌سازی» (SAF) با نوشتن ارائه‌دهنده سند سفارشی دردسترس قرار دهید. این صفحه نحوه ایجاد ارائه‌دهنده سند سفارشی را شرح می‌دهد.

برای اطلاعات بیشتر درباره نحوه عملکرد Storage Access Framework، به نمای کلی Storage Access Framework مراجعه کنید.

مانیفست

برای پیاده‌سازی ارائه‌دهنده سند سفارشی، موارد زیر را به مانیفست برنامه‌تان اضافه کنید:

  • هدف سطح میانای برنامه‌سازی کاربردی ۱۹ یا بالاتر.
  • عنصر <provider> که ارائه‌دهنده فضای ذخیره‌سازی سفارشی شما را اعلام می‌کند.
  • ویژگی android:name روی نام زیرکلاس DocumentsProvider شما تنظیم شده است، که نام کلاس آن است، ازجمله نام بسته:

    com.example.android.storageprovider.MyCloudProvider.

  • مشخصه android:authority، که نام بسته شما است (در این مثال، com.example.android.storageprovider) به‌علاوه نوع ارائه‌دهنده محتوا (documents).
  • مشخصه android:exported روی "true" تنظیم شده است. باید ارائه‌دهنده را صادر کنید تا برنامه‌های دیگر بتوانند آن را ببینند.
  • مشخصه android:grantUriPermissions روی "true" تنظیم شده است. این تنظیم به سیستم اجازه می‌دهد به برنامه‌های دیگر اجازه دسترسی به محتوای ارائه‌دهنده شما را بدهد. برای بحث درباره اینکه چگونه این برنامه‌های دیگر می‌توانند دسترسی خود را به محتوای ارائه‌دهنده شما حفظ کنند، به حفظ اجازه‌ها مراجعه کنید.
  • اجازه MANAGE_DOCUMENTS. به‌طور پیش‌فرض، ارائه‌دهنده برای همه دردسترس است. افزودن این اجازه، ارائه‌دهنده شما را به سیستم محدود می‌کند. این محدودیت برای امنیت مهم است.
  • فیلتر قصدی که شامل کنش android.content.action.DOCUMENTS_PROVIDER باشد تا وقتی سیستم ارائه‌دهندگان را جستجو می‌کند، ارائه‌دهنده شما در انتخابگر نشان داده شود.

در اینجا گزیده‌هایی از یک مانیفست نمونه که شامل یک ارائه‌دهنده است آورده شده است:

<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 نسخه ۴.۳ و پایین‌تر

هدف ACTION_OPEN_DOCUMENT فقط در دستگاه‌های دارای Android نسخه ۴.۴ و بالاتر دردسترس است. اگر می‌خواهید برنامه‌تان از ACTION_GET_CONTENT پشتیبانی کند تا با دستگاه‌هایی که Android نسخه ۴.۳ و پایین‌تر را اجرا می‌کنند سازگار باشد، باید فیلتر قصد ACTION_GET_CONTENT را در مانیفست خود برای دستگاه‌هایی که Android نسخه ۴.۴ یا بالاتر را اجرا می‌کنند غیرفعال کنید. ارائه‌دهنده سند و ACTION_GET_CONTENT باید به‌صورت متقابل انحصاری درنظر گرفته شوند. اگر از هر دو به‌طور هم‌زمان پشتیبانی کنید، برنامه‌تان دو بار در میانای انتخابگر سیستم نشان داده می‌شود و دو روش مختلف برای دسترسی به داده‌های ذخیره‌شده‌تان ارائه می‌دهد. این موضوع برای کاربران گیج‌کننده است.

در اینجا روش توصیه‌شده برای غیرفعال کردن فیلتر قصد ACTION_GET_CONTENT برای دستگاه‌هایی که Android نسخه ۴.۴ یا بالاتر را اجرا می‌کنند آورده شده است:

  1. در فایل منابع bool.xml در بخش res/values/، این خط را اضافه کنید:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. در فایل منابع bool.xml در بخش res/values-v19/، این خط را اضافه کنید:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. نام مستعار فعالیت اضافه کنید تا فیلتر قصد ACTION_GET_CONTENT را برای نسخه‌های ۴.۴ (سطح میانای برنامه‌سازی کاربردی ۱۹) و بالاتر غیرفعال کنید. برای مثال:
    <!-- This activity alias is added so that GET_CONTENT intent-filter
         can be disabled for builds on API level 19 and higher. -->
    <activity-alias android:name="com.android.example.app.MyPicker"
            android:targetActivity="com.android.example.app.MyActivity"
            ...
            android:enabled="@bool/atMostJellyBeanMR2">
        <intent-filter>
            <action android:name="android.intent.action.GET_CONTENT" />
            <category android:name="android.intent.category.OPENABLE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="image/*" />
            <data android:mimeType="video/*" />
        </intent-filter>
    </activity-alias>

قرارداد

معمولاً وقتی ارائه‌دهنده محتوای سفارشی می‌نویسید، یکی از وظایف پیاده‌سازی کلاس‌های قرارداد است، همان‌طور که در راهنمای توسعه‌دهندگان ارائه‌دهندگان محتوا شرح داده شده است. کلاس قرارداد یک کلاس public final است که حاوی تعاریف ثابت برای URI، نام ستون، نوع MIME، و فراداده‌های دیگری است که به ارائه‌دهنده مربوط می‌شود. SAF این کلاس‌های قرارداد را برای شما ارائه می‌دهد، بنابراین نیازی نیست خودتان بنویسید:

برای مثال، در اینجا ستون‌هایی را می‌بینید که ممکن است در مکان‌نما برگردانید وقتی ارائه‌دهنده سند شما برای اسناد یا ریشه پرسمان می‌شود:

کاتلین

private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Root.COLUMN_ROOT_ID,
        DocumentsContract.Root.COLUMN_MIME_TYPES,
        DocumentsContract.Root.COLUMN_FLAGS,
        DocumentsContract.Root.COLUMN_ICON,
        DocumentsContract.Root.COLUMN_TITLE,
        DocumentsContract.Root.COLUMN_SUMMARY,
        DocumentsContract.Root.COLUMN_DOCUMENT_ID,
        DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
)
private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Document.COLUMN_DOCUMENT_ID,
        DocumentsContract.Document.COLUMN_MIME_TYPE,
        DocumentsContract.Document.COLUMN_DISPLAY_NAME,
        DocumentsContract.Document.COLUMN_LAST_MODIFIED,
        DocumentsContract.Document.COLUMN_FLAGS,
        DocumentsContract.Document.COLUMN_SIZE
)

جاوا

private static final String[] DEFAULT_ROOT_PROJECTION =
        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
        Root.COLUMN_AVAILABLE_BYTES,};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};

مکان‌نمای شما برای ریشه باید شامل ستون‌های الزامی خاصی باشد. این ستون‌ها عبارت‌اند از:

مکان‌نما برای اسناد باید شامل ستون‌های الزامی زیر باشد:

ایجاد زیرکلاس DocumentsProvider

مرحله بعدی در نوشتن ارائه‌دهنده سند سفارشی، ایجاد زیرکلاس برای کلاس انتزاعی DocumentsProvider است. حداقل باید روش‌های زیر را پیاده‌سازی کنید:

این‌ها تنها روش‌هایی هستند که اجرای آن‌ها اکیداً الزامی است، اما روش‌های بسیار دیگری وجود دارد که ممکن است بخواهید اجرا کنید. برای جزئیات، به DocumentsProvider بروید.

تعریف ریشه

پیاده‌سازی شما از queryRoots() باید بااستفاده از ستون‌های تعریف‌شده در DocumentsContract.Root، Cursor را که به همه دایرکتوری‌های ریشه ارائه‌دهنده سند شما اشاره می‌کند برگرداند.

در قطعه کد زیر، پارامتر projection نشان‌دهنده فیلدهای خاصی است که تماس‌گیرنده می‌خواهد دریافت کند. این قطعه کد یک مکان‌نمای جدید ایجاد می‌کند و یک ردیف به آن اضافه می‌کند—یک ریشه، یک دایرکتوری سطح بالا، مانند «بارگیری‌ها» یا «تصاویر». اکثر ارائه‌دهندگان فقط یک ریشه دارند. ممکن است بیش‌از یک مورد داشته باشید، برای مثال، درصورت وجود چند حساب کاربری. در این صورت، فقط یک ردیف دوم به مکان‌نما اضافه کنید.

کاتلین

override fun queryRoots(projection: Array<out String>?): Cursor {
    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    val result = MatrixCursor(resolveRootProjection(projection))

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    result.newRow().apply {
        add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)

        // You can provide an optional summary, which helps distinguish roots
        // with the same title. You can also use this field for displaying an
        // user account name.
        add(DocumentsContract.Root.COLUMN_SUMMARY, context.getString(R.string.root_summary))

        // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
        // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
        // recently used documents will show up in the "Recents" category.
        // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
        // shares.
        add(
            DocumentsContract.Root.COLUMN_FLAGS,
            DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
                DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
                DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
        )

        // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
        add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title))

        // This document id cannot change after it's shared.
        add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))

        // The child MIME types are used to filter the roots and only present to the
        // user those roots that contain the desired type somewhere in their file hierarchy.
        add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir))
        add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace)
        add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher)
    }

    return result
}

جاوا

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);

    // You can provide an optional summary, which helps distinguish roots
    // with the same title. You can also use this field for displaying an
    // user account name.
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change after it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir));

    // The child MIME types are used to filter the roots and only present to the
    // user those roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, baseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

اگر ارائه‌دهنده سند شما به مجموعه‌ای پویا از ریشه‌ها متصل می‌شود—برای مثال، به یک دستگاه USB که ممکن است قطع شود یا حسابی که کاربر می‌تواند از آن خارج شود—می‌توانید رابط کاربری سند را به‌روز کنید تا بااستفاده از روش ContentResolver.notifyChange() با آن تغییرات همگام بماند، همان‌طور که در قطعه کد زیر نشان داده شده است.

کاتلین

val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY)
context.contentResolver.notifyChange(rootsUri, null)

جاوا

Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY);
context.getContentResolver().notifyChange(rootsUri, null);

فهرست کردن اسناد در ارائه‌دهنده

پیاده‌سازی شما از queryChildDocuments() باید Cursor را برگرداند که بااستفاده از ستون‌های تعریف‌شده در DocumentsContract.Document به همه فایل‌های موجود در دایرکتوری مشخص‌شده اشاره کند.

وقتی کاربر ریشه شما را در واسط کاربر انتخاب‌گر انتخاب می‌کند، این روش فراخوانی می‌شود. این روش فرزندان شناسه سند مشخص‌شده توسط COLUMN_DOCUMENT_ID را بازیابی می‌کند. سپس هر زمان که کاربر زیرفهرست راهنمایی را در ارائه‌دهنده اسناد شما انتخاب کند، سیستم این روش را فراخوانی می‌کند.

این قطعه کد مکان‌نمای جدیدی با ستون‌های درخواستی ایجاد می‌کند، سپس اطلاعات مربوط به هر فرزند مستقیم در دایرکتوری والد را به مکان‌نما اضافه می‌کند. فرزند می‌تواند تصویر، فهرست راهنمای دیگر، یا هر فایل دیگری باشد:

کاتلین

override fun queryChildDocuments(
        parentDocumentId: String?,
        projection: Array<out String>?,
        sortOrder: String?
): Cursor {
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        val parent: File = getFileForDocId(parentDocumentId)
        parent.listFiles()
                .forEach { file ->
                    includeFile(this, null, file)
                }
    }
}

جاوا

@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
                              String sortOrder) throws FileNotFoundException {

    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    final File parent = getFileForDocId(parentDocumentId);
    for (File file : parent.listFiles()) {
        // Adds the file's display name, MIME type, size, and so on.
        includeFile(result, null, file);
    }
    return result;
}

دریافت اطلاعات سند

پیاده‌سازی شما از queryDocument() باید Cursor را برگرداند که به فایل مشخص‌شده اشاره می‌کند، بااستفاده از ستون‌های تعریف‌شده در DocumentsContract.Document.

روش queryDocument() همان اطلاعاتی را برمی‌گرداند که در queryChildDocuments() ارسال شده است، اما برای یک فایل خاص:

کاتلین

override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
    // Create a cursor with the requested projection, or the default projection.
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        includeFile(this, documentId, null)
    }
}

جاوا

@Override
public Cursor queryDocument(String documentId, String[] projection) throws
        FileNotFoundException {

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

ارائه‌دهنده سند شما همچنین می‌تواند با لغو کردن روش DocumentsProvider.openDocumentThumbnail() و افزودن پرچم FLAG_SUPPORTS_THUMBNAIL به فایل‌های پشتیبانی‌شده، ریزعکس‌هایی برای سند ارائه دهد. تکه‌کد زیر مثالی از نحوه پیاده‌سازی DocumentsProvider.openDocumentThumbnail() ارائه می‌دهد.

کاتلین

override fun openDocumentThumbnail(
        documentId: String?,
        sizeHint: Point?,
        signal: CancellationSignal?
): AssetFileDescriptor {
    val file = getThumbnailFileForDocId(documentId)
    val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    return AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH)
}

جاوا

@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint,
                                                     CancellationSignal signal)
        throws FileNotFoundException {

    final File file = getThumbnailFileForDocId(documentId);
    final ParcelFileDescriptor pfd =
        ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
}

احتیاط: ارائه‌دهنده سند نباید تصاویر ریزعکسی را برگرداند که اندازه آن‌ها بیشتر از دو برابر اندازه مشخص‌شده توسط پارامتر sizeHint باشد.

باز کردن سند

باید openDocument() را پیاده‌سازی کنید تا ParcelFileDescriptor نشان‌دهنده فایل مشخص‌شده را برگرداند. برنامه‌های دیگر می‌توانند از ParcelFileDescriptor برگردانده‌شده برای جاری‌سازی داده‌ها استفاده کنند. سیستم این روش را پس‌از انتخاب فایل توسط کاربر فراخوانی می‌کند، و برنامه کارخواه با فراخوانی openFileDescriptor() درخواست دسترسی به آن را می‌دهد. برای مثال:

کاتلین

override fun openDocument(
        documentId: String,
        mode: String,
        signal: CancellationSignal
): ParcelFileDescriptor {
    Log.v(TAG, "openDocument, mode: $mode")
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    val file: File = getFileForDocId(documentId)
    val accessMode: Int = ParcelFileDescriptor.parseMode(mode)

    val isWrite: Boolean = mode.contains("w")
    return if (isWrite) {
        val handler = Handler(context.mainLooper)
        // Attach a close listener if the document is opened in write mode.
        try {
            ParcelFileDescriptor.open(file, accessMode, handler) {
                // Update the file with the cloud server. The client is done writing.
                Log.i(TAG, "A file with id $documentId has been closed! Time to update the server.")
            }
        } catch (e: IOException) {
            throw FileNotFoundException(
                    "Failed to open document with id $documentId and mode $mode"
            )
        }
    } else {
        ParcelFileDescriptor.open(file, accessMode)
    }
}

جاوا

@Override
public ParcelFileDescriptor openDocument(final String documentId,
                                         final String mode,
                                         CancellationSignal signal) throws
        FileNotFoundException {
    Log.v(TAG, "openDocument, mode: " + mode);
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    final File file = getFileForDocId(documentId);
    final int accessMode = ParcelFileDescriptor.parseMode(mode);

    final boolean isWrite = (mode.indexOf('w') != -1);
    if(isWrite) {
        // Attach a close listener if the document is opened in write mode.
        try {
            Handler handler = new Handler(getContext().getMainLooper());
            return ParcelFileDescriptor.open(file, accessMode, handler,
                        new ParcelFileDescriptor.OnCloseListener() {
                @Override
                public void onClose(IOException e) {

                    // Update the file with the cloud server. The client is done
                    // writing.
                    Log.i(TAG, "A file with id " +
                    documentId + " has been closed! Time to " +
                    "update the server.");
                }

            });
        } catch (IOException e) {
            throw new FileNotFoundException("Failed to open document with id"
            + documentId + " and mode " + mode);
        }
    } else {
        return ParcelFileDescriptor.open(file, accessMode);
    }
}

اگر ارائه‌دهنده سند شما فایل‌ها را جاری‌سازی می‌کند یا ساختارهای داده پیچیده را مدیریت می‌کند، پیاده‌سازی روش‌های createReliablePipe() یا createReliableSocketPair() را درنظر بگیرید. این روش‌ها به شما امکان می‌دهند جفتی از اشیاء ParcelFileDescriptor ایجاد کنید، که در آن می‌توانید یکی را برگردانید و دیگری را ازطریق ParcelFileDescriptor.AutoCloseOutputStream یا ParcelFileDescriptor.AutoCloseInputStream ارسال کنید.

پشتیبانی از اسناد اخیر و جستجو

می‌توانید با لغو کردن روش queryRecentDocuments() و برگرداندن FLAG_SUPPORTS_RECENTS، فهرستی از اسنادی را که اخیراً در ریشه ارائه‌دهنده سند شما اصلاح شده‌اند ارائه دهید. تکه‌کد زیر نمونه‌ای از نحوه پیاده‌سازی روش‌های queryRecentDocuments() را نشان می‌دهد.

کاتلین

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

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

    val parent: File = getFileForDocId(rootId)

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

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

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

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

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

جاوا

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

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

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

    final File parent = getFileForDocId(rootId);

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

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

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

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

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

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

با بارگیری نمونه کد StorageProvider می‌توانید کد کامل قطعه بالا را دریافت کنید.

پشتیبانی از ایجاد سند

می‌توانید به برنامه‌های کارخواه اجازه دهید در ارائه‌دهنده سندتان فایل ایجاد کنند. اگر برنامه کارخواه ACTION_CREATE_DOCUMENT ارسال کند، ارائه‌دهنده سند شما می‌تواند به آن برنامه کارخواه اجازه دهد اسناد جدیدی در ارائه‌دهنده سند ایجاد کند.

برای پشتیبانی از ایجاد سند، ریشه شما باید پرچم FLAG_SUPPORTS_CREATE را داشته باشد. دایرکتوری‌هایی که اجازه می‌دهند فایل‌های جدید در آن‌ها ایجاد شود باید پرچم FLAG_DIR_SUPPORTS_CREATE داشته باشند.

ارائه‌دهنده سند شما نیز باید روش createDocument() را پیاده‌سازی کند. وقتی کاربر دایرکتوری‌ای را در ارائه‌دهنده سند شما برای ذخیره کردن فایل جدید انتخاب می‌کند، ارائه‌دهنده سند تماسی با createDocument() دریافت می‌کند. درون پیاده‌سازی روش createDocument()، COLUMN_DOCUMENT_ID جدیدی برای فایل برمی‌گردانید. سپس برنامه کارخواه می‌تواند از آن شناسه برای دریافت دستگیره فایل استفاده کند و درنهایت، openDocument() را برای نوشتن در فایل جدید فراخوانی کند.

تکه‌کد زیر نحوه ایجاد فایل جدید در ارائه‌دهنده سند را نشان می‌دهد.

کاتلین

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

    return getDocIdForFile(file)
}

جاوا

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

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

با بارگیری نمونه کد StorageProvider می‌توانید کد کامل را برای قطعه بالا دریافت کنید.

پشتیبانی از ویژگی‌های مدیریت سند

علاوه‌بر باز کردن، ایجاد کردن، و مشاهده کردن فایل‌ها، ارائه‌دهنده سند شما می‌تواند به برنامه‌های کارخواه اجازه دهد تا فایل‌ها را تغییر نام دهند، کپی کنند، انتقال دهند، و حذف کنند. برای افزودن عملکرد مدیریت سند به ارائه‌دهنده سند، پرچمی به ستون COLUMN_FLAGS سند اضافه کنید تا عملکرد پشتیبانی‌شده را نشان دهد. همچنین باید روش مربوطه کلاس DocumentsProvider را پیاده‌سازی کنید.

جدول زیر پرچم COLUMN_FLAGS و روش DocumentsProvider را ارائه می‌دهد که ارائه‌دهنده اسناد باید برای نمایش ویژگی‌های خاص پیاده‌سازی کند.

ویژگی پرچم روش
حذف کردن یک فایل FLAG_SUPPORTS_DELETE deleteDocument()
تغییر نام فایل FLAG_SUPPORTS_RENAME renameDocument()
کپی کردن فایل در دایرکتوری والد جدید در ارائه‌دهنده سند FLAG_SUPPORTS_COPY copyDocument()
انتقال فایل از یک فهرست به فهرستی دیگر در ارائه‌دهنده سند FLAG_SUPPORTS_MOVE moveDocument()
برداشتن فایل از دایرکتوری والد FLAG_SUPPORTS_REMOVE removeDocument()

پشتیبانی از فایل‌های مجازی و قالب‌های فایل جایگزین

فایل‌های مجازی، ویژگی‌ای که در Android 7.0 (سطح API‏ ۲۴) معرفی شد، به ارائه‌دهندگان سند اجازه می‌دهد دسترسی مشاهده به فایل‌هایی را که نمایش بایت‌کد مستقیم ندارند ارائه دهند. برای اینکه برنامه‌های دیگر بتوانند فایل‌های مجازی را مشاهده کنند، ارائه‌دهنده سند شما باید نمایش فایل بازشدنی جایگزینی برای فایل‌های مجازی ایجاد کند.

برای مثال، فرض کنید ارائه‌دهنده سند حاوی قالب فایلی است که برنامه‌های دیگر نمی‌توانند مستقیماً آن را باز کنند، درواقع یک فایل مجازی است. وقتی برنامه مشتری قصد ACTION_VIEW را بدون دسته CATEGORY_OPENABLE ارسال می‌کند، کاربران می‌توانند این فایل‌های مجازی را در ارائه‌دهنده سند برای مشاهده انتخاب کنند. سپس ارائه‌دهنده سند فایل مجازی را در قالب فایل متفاوت اما قابل‌باز شدن، مثل تصویر، برمی‌گرداند. سپس برنامه کارخواه می‌تواند فایل مجازی را برای مشاهده کاربر باز کند.

برای اعلام اینکه سندی در ارائه‌دهنده مجازی است، باید پرچم FLAG_VIRTUAL_DOCUMENT را به فایل برگردانده‌شده توسط روش queryDocument() اضافه کنید. این پرچم به برنامه‌های کارخواه هشدار می‌دهد که فایل نمایش بایت‌کد مستقیم ندارد و نمی‌تواند مستقیماً باز شود.

اگر اعلام کنید که فایلی در ارائه‌دهنده سند شما مجازی است، اکیداً توصیه می‌شود آن را در نوع MIME دیگری مانند تصویر یا PDF دردسترس قرار دهید. ارائه‌دهنده سند انواع MIME جایگزینی را که برای مشاهده فایل مجازی پشتیبانی می‌کند با لغو کردن روش getDocumentStreamTypes() اعلام می‌کند. وقتی برنامه‌های کارخواه getStreamTypes(android.net.Uri, java.lang.String) را فراخوانی می‌کنند، سیستم getDocumentStreamTypes() ارائه‌دهنده سند را فراخوانی می‌کند. سپس روش getDocumentStreamTypes() آرایه‌ای از انواع MIME جایگزین را که ارائه‌دهنده سند برای فایل پشتیبانی می‌کند برمی‌گرداند.

پس‌از اینکه کارخواه تشخیص داد ارائه‌دهنده سند می‌تواند سند را در قالب فایل قابل‌مشاهده‌ای تولید کند، برنامه کارخواه روش openTypedAssetFileDescriptor() را فرامی‌خواند که به‌طور داخلی روش openTypedDocument() ارائه‌دهنده سند را فرامی‌خواند. ارائه‌دهنده سند فایل را در قالب فایل درخواستی به برنامه کارخواه برمی‌گرداند.

تکه‌کد زیر پیاده‌سازی ساده‌ای از روش‌های getDocumentStreamTypes() و openTypedDocument() را نشان می‌دهد.

کاتلین

var SUPPORTED_MIME_TYPES : Array<String> = arrayOf("image/png", "image/jpg")
override fun openTypedDocument(
        documentId: String?,
        mimeTypeFilter: String,
        opts: Bundle?,
        signal: CancellationSignal?
): AssetFileDescriptor? {
    return try {
        // Determine which supported MIME type the client app requested.
        when(mimeTypeFilter) {
            "image/jpg" -> openJpgDocument(documentId)
            "image/png", "image/*", "*/*" -> openPngDocument(documentId)
            else -> throw IllegalArgumentException("Invalid mimeTypeFilter $mimeTypeFilter")
        }
    } catch (ex: Exception) {
        Log.e(TAG, ex.message)
        null
    }
}

override fun getDocumentStreamTypes(documentId: String, mimeTypeFilter: String): Array<String> {
    return when (mimeTypeFilter) {
        "*/*", "image/*" -> {
            // Return all supported MIME types if the client app
            // passes in '*/*' or 'image/*'.
            SUPPORTED_MIME_TYPES
        }
        else -> {
            // Filter the list of supported mime types to find a match.
            SUPPORTED_MIME_TYPES.filter { it == mimeTypeFilter }.toTypedArray()
        }
    }
}

جاوا

public static String[] SUPPORTED_MIME_TYPES = {"image/png", "image/jpg"};

@Override
public AssetFileDescriptor openTypedDocument(String documentId,
    String mimeTypeFilter,
    Bundle opts,
    CancellationSignal signal) {

    try {

        // Determine which supported MIME type the client app requested.
        if ("image/png".equals(mimeTypeFilter) ||
            "image/*".equals(mimeTypeFilter) ||
            "*/*".equals(mimeTypeFilter)) {

            // Return the file in the specified format.
            return openPngDocument(documentId);

        } else if ("image/jpg".equals(mimeTypeFilter)) {
            return openJpgDocument(documentId);
        } else {
            throw new IllegalArgumentException("Invalid mimeTypeFilter " + mimeTypeFilter);
        }

    } catch (Exception ex) {
        Log.e(TAG, ex.getMessage());
    } finally {
        return null;
    }
}

@Override
public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) {

    // Return all supported MIME tyupes if the client app
    // passes in '*/*' or 'image/*'.
    if ("*/*".equals(mimeTypeFilter) ||
        "image/*".equals(mimeTypeFilter)) {
        return SUPPORTED_MIME_TYPES;
    }

    ArrayList requestedMimeTypes = new ArrayList&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();
}

امنیت

فرض کنید ارائه‌دهنده سند شما یک سرویس فضای ذخیره‌سازی ابری محافظت‌شده با گذرواژه است و می‌خواهید مطمئن شوید که کاربران قبل‌از شروع هم‌رسانی فایل‌هایشان به سیستم وارد شده‌اند. اگر کاربر به سیستم وارد نشده باشد، برنامه‌تان باید چه کاری انجام دهد؟ راه‌حل این است که در پیاده‌سازی queryRoots()، ریشه صفر برگردانده شود. یعنی مکان‌نمای ریشه خالی:

کاتلین

override fun queryRoots(projection: Array<out String>): Cursor {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

جاوا

public Cursor queryRoots(String[] projection) throws FileNotFoundException {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
}

مرحله دیگر تماس با getContentResolver().notifyChange() است. ‫DocumentsContract را به‌یاد می‌آورید؟ از آن برای ساختن این URI استفاده می‌کنیم. قطعه کد زیر به سیستم می‌گوید که هرگاه وضعیت ورود به سیستم کاربر تغییر کرد، ریشه‌های ارائه‌دهنده سند شما را پُرسمان کند. اگر کاربر به سیستم وارد نشده باشد، فراخوانی queryRoots() مکان‌نمای خالی برمی‌گرداند، همان‌طور که در بالا نشان داده شده است. این کار تضمین می‌کند که اسناد ارائه‌دهنده فقط درصورتی دردسترس باشد که کاربر به سیستم ارائه‌دهنده وارد شده باشد.

کاتلین

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

جاوا

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

برای کد نمونه مربوط به این صفحه، به این موارد مراجعه کنید:

برای ویدیوهای مرتبط با این صفحه، به این موارد مراجعه کنید:

برای اطلاعات مرتبط بیشتر، به این مقاله مراجعه کنید: