אם אתם מפתחים אפליקציה שמספקת שירותי אחסון לקבצים (כמו שירות שמור בענן), אפשר להפוך את הקבצים לזמינים באמצעות Storage Access Framework (SAF) באמצעות כתיבת ספק מסמכים בהתאמה אישית. בדף הזה נסביר איך ליצור ספק מסמכים בהתאמה אישית.
למידע נוסף על אופן הפעולה של Storage Access Framework אפשר לעיין במאמר בנושא סקירה כללית על Storage Access Framework
מניפסט
כדי להטמיע ספק מסמכים מותאם אישית, צריך להוסיף את הפרטים הבאים לקובץ של האפליקציה מניפסט:
- יעד ברמת API 19 ומעלה.
- רכיב
<provider>
שמצהיר על האחסון בהתאמה אישית ספק. -
המאפיין
android:name
שמוגדר לשם של מחלקה משנית,DocumentsProvider
זהו שם המחלקה שלו, כולל שם החבילה:com.example.android.storageprovider.MyCloudProvider
. -
המאפיין
android:authority
, שהוא שם החבילה שלכם (בדוגמה הזאת,com.example.android.storageprovider
) וגם סוג ספק התוכן (documents
). - המאפיין
android:exported
מוגדר כ-"true"
. עליכם לייצא את הספק כדי שאפליקציות אחרות יוכלו לראות אותו. - המאפיין
android:grantUriPermissions
מוגדר לערך"true"
. ההגדרה הזו מאפשרת למערכת להעניק לאפליקציות אחרות גישה לתוכן בספק שלכם. לדיון על הדרכים שבהן אפשר להשתמש באפליקציות האלה להמשיך את הגישה שלהם לתוכן מהספק, להתמיד הרשאות. - ההרשאה
MANAGE_DOCUMENTS
. כברירת מחדל, הספק זמין לכולם. הוספת ההרשאה הזו מגבילה את הספק למערכת. ההגבלה הזו חשובה מטעמי אבטחה. - מסנן Intent שכולל את
הפעולה
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 ומטה
Intent מסוג ACTION_OPEN_DOCUMENT
זמין רק
במכשירים עם Android 4.4 ומעלה.
כדי שהאפליקציה תתמוך ב-ACTION_GET_CONTENT
כדי להתאים למכשירים שבהם פועלת מערכת Android 4.3 ומטה, עליך
השבתה של מסנן Intent ACTION_GET_CONTENT
ב-
המניפסט עבור מכשירים שבהם פועלת מערכת Android 4.4 ומעלה. א'
יש להביא בחשבון את ספק המסמך ואת ACTION_GET_CONTENT
בלעדיים. אם אתם תומכים בשתיהן בו-זמנית, האפליקציה שלכם
מופיעה פעמיים בממשק המשתמש של בורר המערכת, ומציעה שתי דרכים שונות לגשת
של הנתונים השמורים שלכם. מצב כזה מבלבל את המשתמשים.
זו הדרך המומלצת להשבתת
מסנן Intent אחד (ACTION_GET_CONTENT
) לפי מכשירים
מערכת הפעלה Android מגרסה 4.4 ומעלה:
- בקובץ המשאבים
bool.xml
בקטעres/values/
, יש להוסיף השורה הזו:<bool name="atMostJellyBeanMR2">true</bool>
- בקובץ המשאבים
bool.xml
בקטעres/values-v19/
, יש להוסיף השורה הזו:<bool name="atMostJellyBeanMR2">false</bool>
- הוספת
פעילות
כתובת אימייל חלופית כדי להשבית את ה-Intent
ACTION_GET_CONTENT
לסנן לפי גרסה 4.4 (רמת API 19) ואילך. מוצרים לדוגמה:<!-- This activity alias is added so that GET_CONTENT intent-filter can be disabled for builds on API level 19 and higher. --> <activity-alias android:name="com.android.example.app.MyPicker" android:targetActivity="com.android.example.app.MyActivity" ... android:enabled="@bool/atMostJellyBeanMR2"> <intent-filter> <action android:name="android.intent.action.GET_CONTENT" /> <category android:name="android.intent.category.OPENABLE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="image/*" /> <data android:mimeType="video/*" /> </intent-filter> </activity-alias>
חוזים
בדרך כלל כשכותבים ספק תוכן מותאם אישית, אחת מהמשימות היא
שימוש בסוגים של חוזים, כפי שמתואר
המדריך למפתחים של ספקי תוכן. מחלקה לפי חוזה היא מחלקה public final
שמכיל הגדרות קבועות למזהי ה-URI, לשמות העמודות, לסוגי MIME
מטא-נתונים אחרים שקשורים לספק. SAF
מספק לכם את הסוגים האלה של החוזה, כך שלא תצטרכו לכתוב
owner:
לדוגמה, אלו העמודות שאפשר להחזיר בסמן ספק המסמך מבקש לקבל מסמכים או הרשאות לרמה הבסיסית (root):
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
מייצג את
שדות ספציפיים שהמתקשר רוצה לחזור. קטע הקוד יוצר סמן חדש
ומוסיפה לו שורה אחת – שורש אחד, ספרייה ברמה עליונה,
הורדות או תמונות. לרוב הספקים יש רק הרמה הבסיסית (root) אחת. יכול להיות שיש לכם יותר מאישור אחד,
לדוגמה, במקרה של מספר חשבונות משתמשים. במקרה הזה, פשוט צריך להוסיף
את השורה השנייה לסמן.
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
.
מתבצעת קריאה לשיטה הזו כשהמשתמש בוחר את הרמה הבסיסית (root) בממשק המשתמש של הבורר.
השיטה מאחזרת את הצאצאים של מזהה המסמך שצוין על ידי
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
.
תמיכה במסמכים מהזמן האחרון ובחיפוש
אפשר להוסיף רשימה של מסמכים ששונו לאחרונה ברמה הבסיסית (root) של
של ספק המסמכים באמצעות שינוי
אמצעי תשלום אחד (queryRecentDocuments()
) וחוזרים
FLAG_SUPPORTS_RECENTS
,
קטע הקוד הבא מדגים איך להטמיע את התג
queryRecentDocuments()
methods.
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
Intent, ספק המסמכים יכול לאפשר לאפליקציית הלקוח ליצור
מסמכים חדשים אצל ספק המסמך.
כדי לתמוך ביצירת מסמך, הרמה הבסיסית (root) צריכה להיות
סימון 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
)
כדי לציין את הפונקציונליות הנתמכת. צריך גם להטמיע
ה-method המתאים של DocumentsProvider
בכיתה.
הטבלה הבאה מספקת את הפרטים הבאים:
דגל COLUMN_FLAGS
והשיטה DocumentsProvider
,
שהספק צריך להטמיע כדי לחשוף תכונות ספציפיות.
תכונה | סימון | שיטה |
---|---|---|
מחיקת קובץ |
FLAG_SUPPORTS_DELETE
|
deleteDocument()
|
שינוי שם של קובץ |
FLAG_SUPPORTS_RENAME
|
renameDocument()
|
העתקת קובץ לספריית הורה חדשה מתוך ספק המסמך |
FLAG_SUPPORTS_COPY
|
copyDocument()
|
העברת קובץ מספרה אחת לספרייה אחרת בתוך ספק המסמך |
FLAG_SUPPORTS_MOVE
|
moveDocument()
|
הסרת קובץ מספריית ההורה שלו |
FLAG_SUPPORTS_REMOVE
|
removeDocument()
|
תמיכה בקבצים וירטואליים ובפורמטים חלופיים של קבצים
קבצים וירטואליים, תכונה שהושקה ב-Android 7.0 (רמת API 24), מאפשרת לספקי מסמכים כדי לספק גישת צפייה לקבצים שאין להם ייצוג בייטקוד ישיר. כדי לאפשר לאפליקציות אחרות להציג קבצים וירטואליים: ספק המסמך צריך להפיק קובץ חלופי שניתן לפתוח של הקבצים הווירטואליים.
לדוגמה, נניח שספק מסמך מכיל קובץ
פורמט שאפליקציות אחרות לא יכולות לפתוח ישירות, למעשה קובץ וירטואלי.
כשאפליקציית לקוח שולחת Intent מסוג ACTION_VIEW
בלי הקטגוריה CATEGORY_OPENABLE
,
המשתמשים יוכלו לבחור בקבצים הווירטואליים האלה בתוך ספק המסמך
לצפייה. לאחר מכן ספק המסמך מחזיר את הקובץ הווירטואלי
בפורמט קובץ שונה אך פתוח, כמו תמונה.
לאחר מכן אפליקציית הלקוח יכולה לפתוח את הקובץ הווירטואלי ולהציג אותו למשתמש.
כדי להצהיר שמסמך בספק הוא וירטואלי, צריך להוסיף את
FLAG_VIRTUAL_DOCUMENT
בקובץ המוחזר על ידי
queryDocument()
. הסימון הזה מתריע לאפליקציות לקוח על כך שלקובץ אין מזהה ישיר
ייצוג בייטקוד (bytecode) ולא ניתן לפתוח אותו באופן ישיר.
אם מצהירים שקובץ המסמך בספק המסמכים הוא וירטואלי,
מומלץ מאוד להפוך אותו לזמין
סוג MIME, כמו תמונה או קובץ PDF. ספק המסמך
הצהרה על סוגי MIME החלופיים
תומך בהצגת קובץ וירטואלי באמצעות עקיפת
getDocumentStreamTypes()
. כשאפליקציות לקוח מבצעות קריאה
getStreamTypes(android.net.Uri, java.lang.String)
ל-method, המערכת קוראת
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<>(); // Iterate over the list of supported mime types to find a match. for (int i=0; i < 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 Storage Access Framework: ספק
- Storage Access Framework: פיתוח DocumentsProvider
- קבצים וירטואליים ב-Storage Access Framework
מידע נוסף בנושא זמין במאמרים הבאים: