إنشاء موفّر مستند مخصّص

إذا كنت تطور تطبيقًا يوفر خدمات تخزين للملفات (مثل خدمة حفظ في السحابة الإلكترونية)، يمكنك إتاحة ملفاتك من خلال إطار عمل الوصول إلى مساحة التخزين (SAF) من خلال كتابة موفّر مستندات مخصّص. توضّح هذه الصفحة طريقة إنشاء موفِّر مستندات مخصَّص.

لمزيد من المعلومات حول آلية عمل "إطار عمل الوصول إلى مساحة التخزين"، يُرجى الاطّلاع على نظرة عامة على "إطار عمل الوصول إلى مساحة التخزين".

البيان

لتنفيذ موفِّر مستندات مخصّص، أضِف ما يلي إلى بيان تطبيقك:

  • يجب استهداف مستوى واجهة برمجة التطبيقات 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>

دعم الأجهزة التي تعمل بنظام التشغيل Android 4.3 والإصدارات الأقدم

لا يتوفّر هدف ACTION_OPEN_DOCUMENT إلا على الأجهزة التي تعمل بالإصدار 4.4 من نظام التشغيل Android والإصدارات الأحدث. إذا أردت أن يدعم تطبيقك ACTION_GET_CONTENT لاستيعاب الأجهزة التي تعمل بنظام التشغيل Android 4.3 والإصدارات الأقدم، ينبغي لك إيقاف فلتر أهداف ACTION_GET_CONTENT في البيان للأجهزة التي تعمل بنظام التشغيل Android 4.4 أو الإصدارات الأحدث. يجب اعتبار موفّر المستندات وACTION_GET_CONTENT متنافيًا مع الشريك. وإذا كان كلاهما متوافقًا في الوقت نفسه، سيظهر تطبيقك مرتين في واجهة مستخدم منتقي النظام، ما يوفّر طريقتين مختلفتين للوصول إلى البيانات المخزَّنة. وهذا أمرٌ مربك للمستخدمين.

إليك الطريقة الموصى بها لإيقاف فلتر الأهداف ACTION_GET_CONTENT على الأجهزة التي تعمل بالإصدار 4.4 من نظام التشغيل 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 للإصدارات 4.4 (مستوى واجهة برمجة التطبيقات 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 فئات العقود هذه لك، لذلك لا تحتاج إلى كتابة معلوماتك الخاصة:

على سبيل المثال، إليك الأعمدة التي قد ترجعها في المؤشر عند الاستعلام عن المستندات أو الجذر:

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

يجب أن يتضمّن مؤشر الجذر بعض الأعمدة المطلوبة. هذه الأعمدة هي:

يجب أن يتضمّن مؤشر المستندات الأعمدة المطلوبة التالية:

إنشاء فئة فرعية من DocumentsProvider

الخطوة التالية في كتابة موفّر مستندات مخصّص هي تصنيف الفئة المجرّدة DocumentsProvider في فئة فرعية. على الأقل، يجب عليك تنفيذ الطرق التالية:

هذه هي الطرق الوحيدة التي يتعين عليك تنفيذها بشدة، ولكن هناك العديد من الطرق الأخرى التي قد ترغب في تنفيذها. لمزيد من التفاصيل، يُرجى زيارة DocumentsProvider.

تحديد جذر

يجب أن يعرض تنفيذ queryRoots() خطأ Cursor يشير إلى كل الأدلة الجذرية لموفّر المستندات باستخدام الأعمدة المحدّدة في DocumentsContract.Root.

في المقتطف التالي، تمثل المعلمة projection الحقول المحددة التي يريد المتصل الرجوع إليها. ينشئ المقتطف مؤشرًا جديدًا ويضيف صفًا واحدًا إليه: جذر واحد، أو دليل من المستوى الأعلى، مثل "عمليات التنزيل" أو "الصور". لدى معظم مقدّمي الخدمات جذر واحد فقط. على سبيل المثال، قد يكون لديك أكثر من حساب، في حالة وجود حسابات مستخدمين متعددة. في هذه الحالة، فقط أضف صفًا ثانيًا إلى المؤشر.

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

إذا اتصل موفِّر المستندات بمجموعة ديناميكية من الجذور، على سبيل المثال، بجهاز USB قد يكون غير متصل أو بحساب يمكن للمستخدم تسجيل الخروج منه، يمكنك تعديل واجهة مستخدم المستند للبقاء متزامنة مع هذه التغييرات باستخدام طريقة ContentResolver.notifyChange()، كما هو موضّح في مقتطف الرمز التالي.

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

إدراج المستندات في مقدّم الخدمة

يجب أن يعرض تنفيذ queryChildDocuments() عنصر Cursor الذي يشير إلى جميع الملفات في الدليل المحدّد، باستخدام الأعمدة المحدّدة في DocumentsContract.Document.

يتم استدعاء هذه الطريقة عندما يختار المستخدم الجذر في واجهة مستخدم المنتقي. تسترد الطريقة العناصر الثانوية لمعرّف المستند الذي حدّدته COLUMN_DOCUMENT_ID. بعد ذلك، يطلب النظام هذه الطريقة في أي وقت يختار فيه المستخدم دليلاً فرعيًا ضمن موفِّر المستندات.

ينشئ هذا المقتطف مؤشرًا جديدًا باستخدام الأعمدة المطلوبة، ثم يضيف إلى المؤشر معلومات عن كل عنصر ثانوي مباشر في الدليل الرئيسي. يمكن أن يكون "الفرع" صورة أو دليلاً آخر، أي ملف:

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

الحصول على معلومات المستند

يجب أن يؤدي تنفيذ queryDocument() إلى عرض عنصر "Cursor" يشير إلى الملف المحدَّد، باستخدام الأعمدة المحدّدة في DocumentsContract.Document.

تعرض الطريقة queryDocument() المعلومات نفسها التي تم تمريرها في queryChildDocuments()، ولكن بالنسبة إلى ملف محدّد:

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

يمكن لموفّر المستندات أيضًا توفير صور مصغّرة للمستند من خلال إلغاء طريقة DocumentsProvider.openDocumentThumbnail() وإضافة العلامة FLAG_SUPPORTS_THUMBNAIL إلى الملفات المتوافقة. يقدم مقتطف الرمز التالي مثالاً عن كيفية تنفيذ DocumentsProvider.openDocumentThumbnail().

Kotlin

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

Java

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

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

تنبيه: يجب ألا يعرض موفّر المستندات صورًا مصغّرة تزيد عن ضعف الحجم الذي تحدّده المعلَمة sizeHint.

فتح مستند

يجب تنفيذ openDocument() لإرجاع ParcelFileDescriptor الذي يمثّل الملف المحدّد. يمكن للتطبيقات الأخرى استخدام السمة ParcelFileDescriptor المعروضة لبث البيانات. يستدعي النظام هذه الطريقة بعد أن يختار المستخدم ملفًا، ويطلب تطبيق العميل الوصول إليه عن طريق استدعاء openFileDescriptor(). مثلاً:

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

إذا كان موفّر المستندات يبث الملفات أو يتعامل مع بُنى بيانات معقدة، ننصح بتنفيذ الإجراءَين createReliablePipe() أو createReliableSocketPair(). تسمح لك هذه الطرق بإنشاء زوج من كائنات ParcelFileDescriptor، حيث يمكنك عرض أحدهما وإرسال الآخر عبر ParcelFileDescriptor.AutoCloseOutputStream أو ParcelFileDescriptor.AutoCloseInputStream.

إتاحة المستندات الحديثة وعمليات البحث

يمكنك تقديم قائمة بالمستندات التي تم تعديلها مؤخرًا ضمن جذر موفّر المستندات من خلال إلغاء طريقة queryRecentDocuments() وعرض FLAG_SUPPORTS_RECENTS. يعرض مقتطف الرمز التالي مثالاً على كيفية تنفيذ طرق queryRecentDocuments().

Kotlin

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

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

    val parent: File = getFileForDocId(rootId)

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

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

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

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

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

Java

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

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

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

    final File parent = getFileForDocId(rootId);

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

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

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

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

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

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

يمكنك الحصول على الرمز الكامل للمقتطف أعلاه عن طريق تنزيل نموذج الرمز StorageProvider.

دعم إنشاء المستندات

يمكنك السماح لتطبيقات العملاء بإنشاء ملفات ضمن موفِّر المستندات. إذا أرسل تطبيق عميل هدف ACTION_CREATE_DOCUMENT، يمكن لموفّر المستندات السماح لتطبيق العميل هذا بإنشاء مستندات جديدة ضمن موفّر المستندات.

لإتاحة إنشاء المستندات، يجب أن يتضمّن الجذر العلامة FLAG_SUPPORTS_CREATE. يجب أن تحتوي الأدلة التي تسمح بإنشاء ملفات جديدة داخلها على العلامة FLAG_DIR_SUPPORTS_CREATE.

على موفِّر المستندات أيضًا استخدام طريقة createDocument(). عندما يختار المستخدم دليلاً ضمن موفِّر المستندات لحفظ ملف جديد، يتلقّى موفِّر المستندات مكالمة إلى createDocument(). ضمن تنفيذ طريقة createDocument()، تعرض COLUMN_DOCUMENT_ID جديدًا للملف. يمكن لتطبيق العميل بعد ذلك استخدام رقم التعريف هذا للحصول على اسم معرِّف للملف، وفي النهاية يطلب من خلاله openDocument() للكتابة إلى الملف الجديد.

يوضّح مقتطف الرمز التالي كيفية إنشاء ملف جديد ضمن موفِّر مستندات.

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.

دعم ميزات إدارة المستندات

بالإضافة إلى فتح الملفات وإنشائها وعرضها، يمكن لموفّر المستندات أيضًا السماح لتطبيقات العملاء بإعادة تسمية الملفات ونسخها ونقلها وحذفها. لإضافة وظيفة إدارة المستندات إلى موفّر المستندات، أضِف علامة إلى عمود 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 (المستوى 24 لواجهة برمجة التطبيقات)، وهي تسمح لموفّري المستندات بتوفير إمكانية الوصول للاطّلاع على الملفات التي لا تملك تمثيلاً مباشرًا لرمز بايت. للسماح للتطبيقات الأخرى بعرض الملفات الافتراضية، على موفِّر المستندات تقديم تمثيل بديل قابل للفتح للملفات الافتراضية.

على سبيل المثال، تخيل أنّ موفِّر المستندات يحتوي على تنسيق ملف لا يمكن للتطبيقات الأخرى فتحه مباشرةً، ويكون في الأساس ملف افتراضي. عندما يرسل تطبيق عميل ACTION_VIEW intent بدون الفئة CATEGORY_OPENABLE، يمكن للمستخدمين اختيار هذه الملفات الافتراضية في موفّر المستندات لعرضها. بعد ذلك، يعرض موفِّر المستند الملف الافتراضي بتنسيق ملف مختلف لكن قابل للفتح، مثل صورة. ويمكن لتطبيق العميل بعد ذلك فتح الملف الافتراضي للمستخدم لعرضه.

للإشارة إلى أنّ المستند في مقدّم الخدمة مستند إلى مستند افتراضي، يجب إضافة العلامة FLAG_VIRTUAL_DOCUMENT إلى الملف الذي تم عرضه من خلال طريقة queryDocument(). تنبّه هذه العلامة تطبيقات العملاء بأنّ الملف لا يتضمّن تمثيلاً مباشرًا لرمز بايت ولا يمكن فتحه مباشرةً.

إذا أشَرت إلى أنّ ملفًا في مقدّم المستندات ملف افتراضي، ننصحك بشدّة بجعله متاحًا بنوع MIME آخر، مثل صورة أو ملف PDF. يعلن موفّر المستندات عن أنواع MIME البديلة التي يتيحها عرض ملف افتراضي من خلال إلغاء طريقة getDocumentStreamTypes(). عندما تستدعي تطبيقات العميل الطريقة getStreamTypes(android.net.Uri, java.lang.String)، يطلب النظام طريقة getDocumentStreamTypes() لموفّر المستندات. تعرض الطريقة getDocumentStreamTypes() بعد ذلك مصفوفة من أنواع MIME البديلة التي يتيحها موفّر المستندات للملف.

بعد أن يحدّد العميل أنّ موفّر المستندات يمكنه إنشاء المستند بتنسيق ملف قابل للعرض، يطلب تطبيق العميل الطريقة openTypedAssetFileDescriptor() التي تستدعي داخليًا طريقة openTypedDocument() لموفّر المستندات. يعرض موفِّر المستندات الملف إلى تطبيق العميل بتنسيق الملف المطلوب.

يوضِّح مقتطف الرمز التالي طريقة تنفيذ بسيطة للطريقتَين getDocumentStreamTypes() وopenTypedDocument().

Kotlin

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

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

Java


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

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

    try {

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

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

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

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

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

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

    ArrayList requestedMimeTypes = new ArrayList&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(). وهذا يعني أن مؤشر جذر فارغ:

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

الخطوة الأخرى هي طلب الرقم getContentResolver().notifyChange(). هل تذكر DocumentsContract؟ نحن نستخدمه لإنشاء عنوان URI هذا. يطلب المقتطف التالي من النظام طلب بحث عن جذور موفِّر المستند كلما تغيرت حالة تسجيل دخول المستخدم. إذا لم يسجّل المستخدم الدخول، ستعرض المكالمة queryRoots() مؤشرًا فارغًا، كما هو موضّح أعلاه. يضمن ذلك عدم توفر مستندات الموفر إلا إذا سجّل المستخدم دخوله إلى الموفر.

Kotlin

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

Java

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

للاطّلاع على رمز نموذجي مرتبط بهذه الصفحة، يُرجى الرجوع إلى:

بالنسبة إلى الفيديوهات ذات الصلة بهذه الصفحة، يُرجى الاطّلاع على:

للحصول على معلومات إضافية ذات صلة، راجع: