إذا كنت تعمل على تطوير تطبيق يوفر خدمات تخزين للملفات (مثل خدمة حفظ في السحابة الإلكترونية)، يمكنك إتاحة ملفاتك من خلال إطار عمل الوصول إلى مساحة التخزين (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 أو إصدار أحدث:
- في ملف الموارد
bool.xml
ضمنres/values/
، أضِف هذا السطر:<bool name="atMostJellyBeanMR2">true</bool>
- في ملف الموارد
bool.xml
ضمنres/values-v19/
، أضِف هذا السطر:<bool name="atMostJellyBeanMR2">false</bool>
- إضافة
النشاط
بديل لإيقاف هدف
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,};
يجب أن يتضمن مؤشر الجذر بعض الأعمدة المطلوبة. هذه الأعمدة هي:
يجب أن يتضمن مؤشر المستندات الأعمدة المطلوبة التالية:
COLUMN_DOCUMENT_ID
COLUMN_DISPLAY_NAME
COLUMN_MIME_TYPE
COLUMN_FLAGS
COLUMN_SIZE
COLUMN_LAST_MODIFIED
إنشاء فئة فرعية من 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 ArrayListl&t;g&t;(); // Iterate over the list of supported mime types to find a match. for (int i=0; i l&t; 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); }
للحصول على رمز برمجي مرتبط بهذه الصفحة، يُرجى الرجوع إلى:
بالنسبة إلى الفيديوهات ذات الصلة بهذه الصفحة، راجع:
- DevBytes: إطار عمل الوصول إلى مساحة التخزين على Android 4.4: موفّر المحتوى
- إطار عمل الوصول إلى مساحة التخزين: إنشاء DocumentsProvider
- الملفات الافتراضية في "إطار عمل الوصول إلى مساحة التخزين"
لمزيد من المعلومات ذات الصلة، راجع:
- إنشاء DocumentsProvider
- فتح تطبيق "الملفات" باستخدام "إطار عمل الوصول إلى مساحة التخزين"
- أساسيات موفّر المحتوى