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

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

برای اطلاعات بیشتر در مورد نحوه عملکرد چارچوب دسترسی به فضای ذخیره سازی، به نمای کلی چارچوب دسترسی به فضای ذخیره سازی مراجعه کنید.

آشکار

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

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

    com.example.android.storageprovider.MyCloudProvider .

  • ویژگی android:authority که نام بسته شما است (در این مثال com.example.android.storageprovider ) به اضافه نوع ارائه دهنده محتوا ( documents ).
  • ویژگی android:exported روی "true" تنظیم شد. باید ارائه دهنده خود را صادر کنید تا سایر برنامه ها بتوانند آن را ببینند.
  • ویژگی android:grantUriPermissions روی "true" تنظیم شده است. این تنظیم به سیستم اجازه می دهد تا به برنامه های دیگر اجازه دسترسی به محتوای ارائه دهنده شما را بدهد. برای بحث در مورد اینکه چگونه این برنامه‌های دیگر می‌توانند دسترسی خود را به محتوای ارائه‌دهنده شما ادامه دهند، به مجوزهای تداوم مراجعه کنید.
  • مجوز MANAGE_DOCUMENTS . به طور پیش فرض یک ارائه دهنده برای همه در دسترس است. افزودن این مجوز، ارائه دهنده شما را به سیستم محدود می کند. این محدودیت برای امنیت مهم است.
  • یک فیلتر هدف که شامل عملکرد 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>

پشتیبانی از دستگاه های دارای اندروید 4.3 و پایین تر

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

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

  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 برای نسخه‌های 4.4 (سطح API 19) و بالاتر، یک نام مستعار فعالیت اضافه کنید. به عنوان مثال:
    <!-- This activity alias is added so that GET_CONTENT intent-filter
         can be disabled for builds on API level 19 and higher. -->
    <activity-alias android:name="com.android.example.app.MyPicker"
            android:targetActivity="com.android.example.app.MyActivity"
            ...
            android:enabled="@bool/atMostJellyBeanMR2">
        <intent-filter>
            <action android:name="android.intent.action.GET_CONTENT" />
            <category android:name="android.intent.category.OPENABLE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="image/*" />
            <data android:mimeType="video/*" />
        </intent-filter>
    </activity-alias>

قراردادها

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

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

کاتلین

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

جاوا

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

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

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

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

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

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

ریشه را تعریف کنید

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

در قطعه زیر، پارامتر 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() همگام بماند. روش 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 برگرداند.

یک سند باز کنید

برای برگرداندن ParcelFileDescriptor که نماینده فایل مشخص شده است، باید openDocument() را پیاده سازی کنید. سایر برنامه ها می توانند از 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 24)، به ارائه‌دهندگان اسناد اجازه می‌دهد تا دسترسی مشاهده فایل‌هایی را که نمایش بایت کد مستقیم ندارند، فراهم کنند. برای فعال کردن سایر برنامه‌ها برای مشاهده فایل‌های مجازی، ارائه‌دهنده سند شما باید یک نمایش فایل قابل باز جایگزین برای فایل‌های مجازی تولید کند.

برای مثال، تصور کنید یک ارائه‌دهنده سند حاوی فرمت فایلی است که سایر برنامه‌ها نمی‌توانند مستقیماً آن را باز کنند، اساساً یک فایل مجازی. وقتی یک برنامه مشتری یک هدف 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);
}

برای مشاهده نمونه کد مربوط به این صفحه به آدرس زیر مراجعه کنید:

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

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