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

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

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

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

لمزيد من المعلومات ذات الصلة، راجع: