ספק התוכן מנהל את הגישה למאגר מרכזי של נתונים. ספק הוא חלק מאפליקציה ל-Android, שלרוב מספקת ממשק משתמש משלו לעבודה עם הנתונים. עם זאת, ספקי התוכן משמשים בעיקר אפליקציות אחרות, שמקבלות גישה לספק באמצעות אובייקט לקוח של ספק. יחד, הספקים ולקוחות הספקים מציעים ממשק סטנדרטי ועקבי לנתונים שמטפל גם בתקשורת בין תהליכים ובגישה מאובטחת לנתונים.
בדרך כלל עובדים עם ספקי תוכן באחד משני התרחישים הבאים: הטמעת קוד כדי לגשת לספק תוכן קיים באפליקציה אחרת, או יצירת ספק תוכן חדש באפליקציה כדי לשתף נתונים עם אפליקציות אחרות.
בדף הזה נסביר את העקרונות הבסיסיים לעבודה עם ספקי תוכן קיימים. למידע נוסף על הטמעת ספקי תוכן באפליקציות שלכם, קראו את המאמר יצירת ספק תוכן.
בנושא הזה נסביר את הנושאים הבאים:
- איך פועלים ספקי התוכן.
- ה-API שבו אתם משתמשים כדי לאחזר נתונים מספק תוכן.
- ה-API שבו משתמשים כדי להוסיף, לעדכן או למחוק נתונים אצל ספק התוכן.
- תכונות API נוספות שעוזרות לעבוד עם ספקים.
סקירה כללית
ספק תוכן מציג נתונים לאפליקציות חיצוניות כטבלה אחת או יותר, שדומות לטבלאות במסד נתונים יחסיים. שורה מייצגת מופע של סוג כלשהו של נתונים שהספק אוסף, וכל עמודה בשורה מייצגת נתון ספציפי שנאסף לגבי מופע.
ספק תוכן מתאם את הגישה לשכבת אחסון הנתונים באפליקציה עבור מספר ממשקי API ורכיבים שונים. כפי שמוצג באיור 1, אלה כוללים:
- שיתוף הגישה לנתוני האפליקציה עם אפליקציות אחרות
- שליחת נתונים לווידג'ט
- הצגת הצעות חיפוש בהתאמה אישית לאפליקציה באמצעות מסגרת החיפוש באמצעות
SearchRecentSuggestionsProvider
- סנכרון נתוני האפליקציה עם השרת באמצעות הטמעה של
AbstractThreadedSyncAdapter
- טעינת נתונים בממשק המשתמש באמצעות
CursorLoader
גישה לספק
כדי לגשת לנתונים בספק תוכן, צריך להשתמש באובייקט ContentResolver
ב-Context
של האפליקציה כדי לתקשר עם הספק כלקוח. אובייקט ContentResolver
מתקשר עם אובייקט הספק, שהוא מופע של כיתה שמטמיעה את ContentProvider
.
אובייקט הספק מקבל בקשות נתונים מהלקוחות, מבצע את הפעולה המבוקשת ומחזיר את התוצאות. לאובייקט הזה יש methods שקוראות ל-methods עם שם זהה באובייקט הספק, מכונה של אחת ממחלקות המשנה הממשיות של ContentProvider
. ה-methods ContentResolver
מספקות את הפונקציות הבסיסיות 'CRUD' (יצירה, אחזור, עדכון ומחיקה) של אחסון מתמיד.
דפוס נפוץ לגישה ל-ContentProvider
מממשק המשתמש הוא שימוש ב-CursorLoader
כדי להריץ שאילתה אסינכרונית ברקע. הערך Activity
או Fragment
בממשק המשתמש קורא ל-CursorLoader
בשאילתה, שמקבלת את הערך ContentProvider
באמצעות ContentResolver
.
כך ממשק המשתמש ימשיך להיות זמין למשתמש בזמן שהשאילתה פועלת. הדפוס הזה כולל אינטראקציה של מספר אובייקטים שונים, וגם מנגנון האחסון הבסיסי, כפי שמתואר באיור 2.
הערה: כדי לגשת לספק, בדרך כלל האפליקציה צריכה לבקש הרשאות ספציפיות בקובץ המניפסט. דפוס ההתפתחות הזה מתואר בפירוט בקטע הרשאות של ספק תוכן.
אחד מהספקים המובנים בפלטפורמת Android הוא ספק מילון המשתמש, שמאחסן את המילים הלא סטנדרטיות שהמשתמש רוצה לשמור. בטבלה 1 מוסבר איך הנתונים עשויים להיראות בטבלה של הספק הזה:
מילים | מזהה אפליקציה | תדירות | שילוב של שפה ואזור | _ID |
---|---|---|---|---|
mapreduce |
user1 | 100 | iw_IL | 1 |
precompiler |
משתמש14 | 200 | fr_FR | 2 |
applet |
user2 | 225 | fr_CA | 3 |
const |
user1 | 255 | נק'_BR | 4 |
int |
user5 | 100 | iw_IL | 5 |
בטבלה 1, כל שורה מייצגת מופע של מילה שלא נמצאת במילון רגיל. כל עמודה מייצגת נתון לגבי המילה הזו, כמו מקום המוצא שבו היא נמצאה לראשונה. כותרות העמודות הן שמות העמודות שמאוחסנים אצל הספק. למשל, כדי להתייחס ללוקאל של שורה מסוימת, צריך לעיין בעמודה locale
שלה. אצל הספק הזה, העמודה _ID
משמשת כעמודה של מפתח ראשי שהספק שומר באופן אוטומטי.
כדי לקבל רשימה של המילים והלוקאלים שלהן מספק המילון של המשתמש,
קוראים ל-ContentResolver.query()
.
ה-method query()
קוראת ל-method ContentProvider.query()
שהוגדרה על ידי ספק המילון של המשתמש. בשורות הקוד הבאות מוצגת קריאה של ContentResolver.query()
:
Kotlin
// Queries the UserDictionary and returns results cursor = contentResolver.query( UserDictionary.Words.CONTENT_URI, // The content URI of the words table projection, // The columns to return for each row selectionClause, // Selection criteria selectionArgs.toTypedArray(), // Selection criteria sortOrder // The sort order for the returned rows )
Java
// Queries the UserDictionary and returns results cursor = getContentResolver().query( UserDictionary.Words.CONTENT_URI, // The content URI of the words table projection, // The columns to return for each row selectionClause, // Selection criteria selectionArgs, // Selection criteria sortOrder); // The sort order for the returned rows
בטבלה 2 מוצג איך הארגומנטים של query(Uri,projection,selection,selectionArgs,sortOrder)
תואמים להצהרת SELECT ב-SQL:
הארגומנט query() |
בחירת מילת מפתח/פרמטר | הערות |
---|---|---|
Uri |
FROM table_name |
השדה Uri ממופה לטבלה בספק בשם table_name. |
projection |
col,col,col,... |
projection הוא מערך עמודות שכלול בכל שורה שאוחזרה.
|
selection |
WHERE col = value |
selection מציין את הקריטריונים לבחירת שורות. |
selectionArgs |
אין מקבילה מדויקת. ארגומנטים של בחירה מחליפים את ה-placeholders של ? בתנאי הבחירה.
|
|
sortOrder |
ORDER BY col,col,... |
sortOrder מציין את הסדר שבו השורות מופיעות ב-Cursor המוחזר.
|
מזהי URI של תוכן
URI של תוכן הוא URI שמזהה נתונים בספק. מזהי URI של תוכן כוללים את השם הסמלי של הספק כולו – הרשות שלו – ושם שמפנה לטבלה – נתיב. כשקוראים לשיטת לקוח כדי לגשת לטבלה בספק, URI התוכן של הטבלה הוא אחד מהארגומנטים.
בשורות הקוד הקודמות, הקבוע CONTENT_URI
מכיל את URI התוכן של הטבלה Words
של ספק המילון של המשתמש. האובייקט ContentResolver
מפרק את הרשות של ה-URI ומשתמש בה כדי לפתור את הספק, על ידי השוואת הרשות לטבלת מערכת של ספקים ידועים. לאחר מכן, ה-ContentResolver
יכול לשלוח את ארגומנטים השאילתה לספק הנכון.
ה-ContentProvider
מ��תמש ב��לק ��נת��ב ��ל ה-URI של התוכן כדי לבחור את הטבלה שאליה רוצים לגשת. בדרך כלל, לכל טבלה שספק חושף יש נתיב משלו.
בשורות הקוד הקודמות, ה-URI המלא של טבלה Words
הוא:
content://user_dictionary/words
- המחרוזת
content://
היא הסכימה, שתמיד נמצאת ומזהה את ה-URI כתוכן. - המחרוזת
user_dictionary
היא הרשות של הספק. - המחרוזת
words
היא הנתיב של הטבלה.
הרבה ספקים מאפשרים לגשת לשורה אחת בטבלה על ידי הוספת ערך מזהה
לסוף ה-URI. לדוגמה, כדי לאחזר שורה שהערך של _ID
הוא
4
מספק מילון המשתמשים, אפשר להשתמש ב-URI התוכן הזה:
Kotlin
val singleUri: Uri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4)
Java
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
בדרך כלל משתמשים בערכי מזהים כשמאחזרים קבוצת שורות ואז רוצים לעדכן או למחוק אחת מהן.
הערה: הכיתות Uri
ו-Uri.Builder
מכילות שיטות נוחות ליצירת אובייקטים של URI תקינים מחרוזות. הכיתה ContentUris
מכילה שיטות נוחות להוספת ערכים של מזהים ל-URI. בקטע הקוד הקודם, ה-withAppendedId()
משמש להוספת מזהה ל-URI של התוכן של ספק מילון המשתמשים.
אחזור נתונים מהספק
בקטע הזה מוסבר איך לאחזר נתונים מספק, תוך שימוש בספק המילון של המשתמש כדוגמה.
לשם הבהרה, קטעי הקוד בקטע הזה קוראים לפונקציה ContentResolver.query()
ב-thread של ממשק המשתמש. עם זאת, בקוד בפועל, צריך לבצע שאילתות באופן אסינכרוני בשרשור נפרד. אפשר להשתמש בכיתה CursorLoader
, שמתוארת בפירוט רב יותר במדריך
Loaders. בנוסף, שורות הקוד הן קטעי קוד בלבד. הן לא מציגות בקשה מלאה.
כדי לאחזר נתונים מספק מסוים, צריך לבצע את השלבים הבסיסיים הבאים:
- מבקשים הרשאת קריאה מהספק.
- מגדירים את הקוד ששולח שאילתה לספק.
שליחת בקשה להרשאת גישה לקריאה
כדי לאחזר נתונים מספק, לאפליקציה צריכה להיות הרשאת קריאה לספק. לא ניתן לבקש את ההרשאה הזו בזמן ריצה. במקום זאת, צריך לציין את הצורך בהרשאה הזו במניפסט באמצעות הרכיב <uses-permission>
ושם ההרשאה המדויק שהוגדר על ידי הספק.
כשמציינים את הרכיב הזה במניפסט, מבקשים את ההרשאה הזו לאפליקציה. כשמשתמשים מתקינים את האפליקציה, הם מעניקים הסכמה מרומזת לבקשה הזו.
במסמכי התיעוד של הספק תוכלו למצוא את השם המדויק של הרשאת הגישה לקריאה של הספק שבו אתם משתמשים, ואת השמות של הרשאות גישה אחרות שהספק משתמש בו.
תפקיד ההרשאות בגישה לספקים מתואר בפירוט רב יותר בקטע הרשאות של ספקי תוכן.
ספק מילון המשתמשים מגדיר את ההרשאה android.permission.READ_USER_DICTIONARY
בקובץ המניפסט שלו, כך שאפליקציה שרוצה לקרוא מהספק חייבת לבקש את ההרשאה הזו.
יצירת השאילתה
השלב הבא באחזור נתונים מספק הוא ליצור שאילתה. קטע הקוד הבא מגדיר משתנים מסוימים לגישה לספק של מילון המשתמשים:
Kotlin
// A "projection" defines the columns that are returned for each row private val mProjection: Array<String> = arrayOf( UserDictionary.Words._ID, // Contract class constant for the _ID column name UserDictionary.Words.WORD, // Contract class constant for the word column name UserDictionary.Words.LOCALE // Contract class constant for the locale column name ) // Defines a string to contain the selection clause private var selectionClause: String? = null // Declares an array to contain selection arguments private lateinit var selectionArgs: Array<String>
Java
// A "projection" defines the columns that are returned for each row String[] mProjection = { UserDictionary.Words._ID, // Contract class constant for the _ID column name UserDictionary.Words.WORD, // Contract class constant for the word column name UserDictionary.Words.LOCALE // Contract class constant for the locale column name }; // Defines a string to contain the selection clause String selectionClause = null; // Initializes an array to contain selection arguments String[] selectionArgs = {""};
קטע הקוד הבא מראה איך להשתמש ב-ContentResolver.query()
, באמצעות ספק מילון המשתמשים כדוגמה. שאילתה של לקוח ספק דומה לשאילתת SQL, והיא מכילה קבוצה של עמודות להחזרה, קבוצה של קריטריונים לבחירה וסדר מיון.
קבוצת העמודות שהשאילתה מחזירה נקראת השלכה, והמשתנה הוא mProjection
.
הביטוי שמציין את השורות לאחזור מפוצל לסעיף בחירה ולארגומנטים של בחירה. תנאי הבחירה הוא שילוב של ביטויים לוגיים ובוליאניים, שמות עמודות וערכים. המשתנה הוא mSelectionClause
. אם מציינים את הפרמטר להחלפה ?
במקום ערך, שיטת השאילתה מאחזרת את הערך מהמערך של ארגומנטים לסינון, שהוא המשתנה mSelectionArgs
.
בקטע הקוד הבא, אם המשתמש לא מזין מילה, תנאי הבחירה מוגדר ל-null
והשאילתה מחזירה את כל המילים בספק. אם המשתמש מזין מילה, תנאי הבחירה מוגדר ל-UserDictionary.Words.WORD + " = ?"
והרכיב הראשון במערך של ארגומנטים לבחירה מוגדר למילה שהמשתמש מזין.
Kotlin
/* * This declares a String array to contain the selection arguments. */ private lateinit var selectionArgs: Array<String> // Gets a word from the UI searchString = searchWord.text.toString() // Insert code here to check for invalid or malicious input // If the word is the empty string, gets everything selectionArgs = searchString?.takeIf { it.isNotEmpty() }?.let { selectionClause = "${UserDictionary.Words.WORD} = ?" arrayOf(it) } ?: run { selectionClause = null emptyArray<String>() } // Does a query against the table and returns a Cursor object mCursor = contentResolver.query( UserDictionary.Words.CONTENT_URI, // The content URI of the words table projection, // The columns to return for each row selectionClause, // Either null or the word the user entered selectionArgs, // Either empty or the string the user entered sortOrder // The sort order for the returned rows ) // Some providers return null if an error occurs, others throw an exception when (mCursor?.count) { null -> { /* * Insert code here to handle the error. Be sure not to use the cursor! * You might want to call android.util.Log.e() to log this error. */ } 0 -> { /* * Insert code here to notify the user that the search is unsuccessful. This isn't * necessarily an error. You might want to offer the user the option to insert a new * row, or re-type the search term. */ } else -> { // Insert code here to do something with the results } }
Java
/* * This defines a one-element String array to contain the selection argument. */ String[] selectionArgs = {""}; // Gets a word from the UI searchString = searchWord.getText().toString(); // Remember to insert code here to check for invalid or malicious input // If the word is the empty string, gets everything if (TextUtils.isEmpty(searchString)) { // Setting the selection clause to null returns all words selectionClause = null; selectionArgs[0] = ""; } else { // Constructs a selection clause that matches the word that the user entered selectionClause = UserDictionary.Words.WORD + " = ?"; // Moves the user's input string to the selection arguments selectionArgs[0] = searchString; } // Does a query against the table and returns a Cursor object mCursor = getContentResolver().query( UserDictionary.Words.CONTENT_URI, // The content URI of the words table projection, // The columns to return for each row selectionClause, // Either null or the word the user entered selectionArgs, // Either empty or the string the user entered sortOrder); // The sort order for the returned rows // Some providers return null if an error occurs, others throw an exception if (null == mCursor) { /* * Insert code here to handle the error. Be sure not to use the cursor! You can * call android.util.Log.e() to log this error. * */ // If the Cursor is empty, the provider found no matches } else if (mCursor.getCount() < 1) { /* * Insert code here to notify the user that the search is unsuccessful. This isn't necessarily * an error. You can offer the user the option to insert a new row, or re-type the * search term. */ } else { // Insert code here to do something with the results }
השאילתה הזו מקבילה להצהרת SQL הבאה:
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
בהצהרת ה-SQL הזו, נעשה שימוש בשמות העמודות בפועל במקום בקבצים קבועים של סוג החוזה.
הגנה מפני קלט זדוני
אם הנתונים שמנוהלים על ידי ספק התוכן נמצאים במסד נתונים של SQL, הכללתם של נתונים חיצוניים לא מהימנים בהצהרות SQL גולמיות עלולה להוביל להזרקת SQL.
נבחן את תנאי הבחירה הבא:
Kotlin
// Constructs a selection clause by concatenating the user's input to the column name var selectionClause = "var = $mUserInput"
Java
// Constructs a selection clause by concatenating the user's input to the column name String selectionClause = "var = " + userInput;
אם תעשו זאת, המשתמש יוכל לצרף שאילתת SQL זדונית להצהרת ה-SQL שלכם.
לדוגמה, המשתמש יכול להזין "nothing; DROP TABLE *;" עבור mUserInput
, וכתוצאה מכ�� תתקבל תניית הבחירה var = nothing; DROP TABLE *;
.
מכיוון שסעיף הבחירה נחשב כהצהרת SQL, הספק עלול למחוק את כל הטבלאות במסד הנתונים הבסיסי של SQLite, אלא אם הספק מוגדר לזהות ניסיונות של החדרת SQL.
כדי למנוע את הבעיה, צריך להשתמש במשפט בחירה שמשתמש ב-?
כפרמטר להחלפה ובמערך נפרד של ארגומנטים מסוג בחירה. כך, הקלט של המשתמש מחויב ישירות לשאילתה במקום להתפרש כחלק מטענת SQL.
מכיוון שהקלט של המשתמש לא מטופל כ-SQL, אי אפשר להחדיר באמצעותו שפת SQL זדונית. במקום להשתמש בשרשור כדי לכלול את הקלט של המשתמש, צריך להשתמש בתנאי הבחירה הזה:
Kotlin
// Constructs a selection clause with a replaceable parameter var selectionClause = "var = ?"
Java
// Constructs a selection clause with a replaceable parameter String selectionClause = "var = ?";
מגדירים את מערך הארגומנטים לסינון כך:
Kotlin
// Defines a mutable list to contain the selection arguments var selectionArgs: MutableList<String> = mutableListOf()
Java
// Defines an array to contain the selection arguments String[] selectionArgs = {""};
מוסיפים ערך למערך של ארגומנטי הבחירה באופן הבא:
Kotlin
// Adds the user's input to the selection argument selectionArgs += userInput
Java
// Sets the selection argument to the user's input selectionArgs[0] = userInput;
הדרך המועדפת לציין בחירה, גם אם הספק לא מבוסס על מסד נתונים של SQL, באמצעות תנאי בחירה שמשתמש ב-?
כפרמטר להחלפה ומערך של מערך ארגומנטים של בחירה.
הצגת תוצאות השאילתה
שיטת הלקוח ContentResolver.query()
תמיד מחזירה Cursor
שמכיל את העמודות שצוינו על ידי הייצוא של השאילתה עבור השורות שתואמות לקריטריונים לבחירה של השאילתה. אובייקט Cursor
מעניק גישת קריאה אקראית לשורות ולעמודות שהוא מכיל.
באמצעות השיטות של Cursor
אפשר לבצע איטרציה על השורות בתוצאות, לקבוע את סוג הנתונים של כל עמודה, לאחזר את הנתונים מעמודה ולבחון מאפיינים אחרים של התוצאות.
חלק מההטמעות של Cursor
מעדכנות את האובייקט באופן אוטומטי כשהנתונים של הספק משתנים, מפעילות שיטות באובייקט הצופה כשה-Cursor
משתנה, או את שניהם.
הערה: ספק יכול להגביל את הגישה לעמודות על סמך אופי האובייקט שמבצע את השאילתה. לדוגמה, הספק של אנשי הקשר מגביל את הגישה של מתאמי סנכרון לעמודות מסוימות, כך שהוא לא מחזיר אותן לפעילות או לשירות.
אם אין שורות שתואמות לקריטריונים לבחירה, הספק מחזיר אובייקט Cursor
שבו הערך של Cursor.getCount()
הוא 0, כלומר סמן ריק.
אם מתרחשת שגיאה פנימית, תוצאות השאילתה תלויות בספק הספציפי. יכול להיות שהיא תחזיר את הערך null
, או שתשליך את הערך Exception
.
מכיוון ש-Cursor
הוא רשימה של שורות, דרך טובה להציג את התוכן של Cursor
היא לקשר אותו ל-ListView
באמצעות SimpleCursorAdapter
.
קטע הקוד הבא ממשיך את הקוד מקטע הקוד הקודם. הוא יוצר אובייקט SimpleCursorAdapter
שמכיל את ה-Cursor
שאוחזר על ידי השאילתה, ומגדיר את האובייקט הזה כמתאם של ListView
.
Kotlin
// Defines a list of columns to retrieve from the Cursor and load into an output row val wordListColumns : Array<String> = arrayOf( UserDictionary.Words.WORD, // Contract class constant containing the word column name UserDictionary.Words.LOCALE // Contract class constant containing the locale column name ) // Defines a list of View IDs that receive the Cursor columns for each row val wordListItems = intArrayOf(R.id.dictWord, R.id.locale) // Creates a new SimpleCursorAdapter cursorAdapter = SimpleCursorAdapter( applicationContext, // The application's Context object R.layout.wordlistrow, // A layout in XML for one row in the ListView mCursor, // The result from the query wordListColumns, // A string array of column names in the cursor wordListItems, // An integer array of view IDs in the row layout 0 // Flags (usually none are needed) ) // Sets the adapter for the ListView wordList.setAdapter(cursorAdapter)
Java
// Defines a list of columns to retrieve from the Cursor and load into an output row String[] wordListColumns = { UserDictionary.Words.WORD, // Contract class constant containing the word column name UserDictionary.Words.LOCALE // Contract class constant containing the locale column name }; // Defines a list of View IDs that receive the Cursor columns for each row int[] wordListItems = { R.id.dictWord, R.id.locale}; // Creates a new SimpleCursorAdapter cursorAdapter = new SimpleCursorAdapter( getApplicationContext(), // The application's Context object R.layout.wordlistrow, // A layout in XML for one row in the ListView mCursor, // The result from the query wordListColumns, // A string array of column names in the cursor wordListItems, // An integer array of view IDs in the row layout 0); // Flags (usually none are needed) // Sets the adapter for the ListView wordList.setAdapter(cursorAdapter);
הערה: כדי לחזור ל-ListView
באמצעות Cursor
, הסמן צריך לכלול עמודה בשם _ID
.
ל��ן, השאילתה שמוצגת למעלה מאחזרת את העמודה _ID
בטבלה Words
, למרות שהיא לא מוצגת ב-ListView
.
האילוץ הזה גם מסביר למה לרוב הספקים יש עמודה _ID
לכל אחת מהטבלאות שלהם.
אחזור נתונים מתוצאות של שאילתות
בנוסף להצגת תוצאות של שאילתות, אפשר להשתמש בהן למשימות אחרות. לדוגמה, אפשר לאחזר איותים מהספק של מילון המשתמש ואז לחפש אותם אצל ספקים אחרים. כדי לעשות זאת, מבצעים איטרציה על השורות ב-Cursor
, כפי שמוצג בדוגמה הבאה:
Kotlin
/* * Only executes if the cursor is valid. The User Dictionary Provider returns null if * an internal error occurs. Other providers might throw an Exception instead of returning null. */ mCursor?.apply { // Determine the column index of the column named "word" val index: Int = getColumnIndex(UserDictionary.Words.WORD) /* * Moves to the next row in the cursor. Before the first movement in the cursor, the * "row pointer" is -1, and if you try to retrieve data at that position you get an * exception. */ while (moveToNext()) { // Gets the value from the column newWord = getString(index) // Insert code here to process the retrieved word ... // End of while loop } }
Java
// Determine the column index of the column named "word" int index = mCursor.getColumnIndex(UserDictionary.Words.WORD); /* * Only executes if the cursor is valid. The User Dictionary Provider returns null if * an internal error occurs. Other providers might throw an Exception instead of returning null. */ if (mCursor != null) { /* * Moves to the next row in the cursor. Before the first movement in the cursor, the * "row pointer" is -1, and if you try to retrieve data at that position you get an * exception. */ while (mCursor.moveToNext()) { // Gets the value from the column newWord = mCursor.getString(index); // Insert code here to process the retrieved word ... // End of while loop } } else { // Insert code here to report an error if the cursor is null or the provider threw an exception }
הטמעות של Cursor
מכילות כמה שיטות 'get' לאחזור סוגים שונים של נתונים מהאובייקט. לדוגמה, בקטע הקוד הקודם נעשה שימוש ב-getString()
. יש להן גם שיטה getType()
שמחזירה ערך שמציין את סוג הנתונים של העמודה.
שחרור משאבים של תוצאות שאילתות
צריך לסגור אובייקטים מסוג Cursor
אם הם כבר לא נחוצים, כדי שהמשאבים המשויכים אליהם ישוחררו מוקדם יותר. אפשר לעשות זאת באמצעות קריאה ל-close()
, או באמצעות הצהרת try-with-resources
בשפת התכנות Java או הפונקצי�� use()
בשפת התכנות Kotlin.
הרשאות של ספקי תוכן
אפליקציה של ספק יכולה לציין הרשאות שאפליקציות אחרות צריכות כדי לגשת לנתונים של הספק. ההרשאות האלה מאפשרות למשתמש לדעת לאילו נתונים האפליקציה מנסה לגשת. בהתאם לדרישות הספק, אפליקציות אחרות מבקשות את ההרשאות שהן צריכות כדי לגשת לספק. משתמשי הקצה רואים את ההרשאות המבוקשות כשהם מתקינים את האפליקציה.
אם באפליקציה של ספק לא צוינו הרשאות, לאפליקציות אחרות אין גישה לנתונים של הספק, אלא אם הספק מיוצא. בנוסף, לרכיבים באפליקציה של הספק תמיד יש גישה מלאה לקריאה ולכתיבה, ללא קשר להרשאות שצוינו.
כדי לאחזר ממנו נתונים, לספק של מילון המשתמש נדרשת ההרשאה android.permission.READ_USER_DICTIONARY
.
לספק יש הרשאה נפרדת android.permission.WRITE_USER_DICTIONARY
להוספה, לעדכון או למחיקה של נתונים.
כדי לקבל את ההרשאות הנדרשות לגישה לספק, האפליקציה מבקשת אותן באמצעות הרכיב <uses-permission>
בקובץ המניפסט שלה. כאשר 'מנהל החבילות של Android' מתקין את האפליקציה, המשתמש
צריך לאשר את כל ההרשאות שהאפליקציה מבקשת. אם המשתמש יאשר אותן, מנהל החבילות ימשיך בהתקנה. אם המשתמש לא יאשר אותן, מנהל החבילות יעצור את ההתקנה.
הרכיב לדוגמה <uses-permission>
מבקש גישה לקריאה לספק של מילון המשתמשים:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">
ההשפעה של ההרשאות על הגישה של הספק מוסברת בפירוט נוסף בטיפים לאבטחה.
הוספה, עדכון ומחיקה של נתונים
בדומה לאופן שבו מאחזרים נתונים מספק, משתמשים גם באינטראקציה בין לקוח של ספק לבין ContentProvider
של הספק כדי לשנות נתונים.
קוראים ל-method של ContentResolver
עם ארגומנטים שמועברים ל-method התואם של ContentProvider
. הספק ולקוח הספק מטפלים באופן אוטומטי באבטחה ובתקשורת בין תהליכים.
הוספת נתונים
כדי להוסיף נתונים לספק, צריך להפעיל את השיטה ContentResolver.insert()
. השיטה הזו מכניסה שורה חדשה לספק ומחזירה URI של תוכן עבור השורה הזו.
ק��ע הקוד הבא מראה איך להוסיף מילה חדשה לספק של מילון המשתמש:
Kotlin
// Defines a new Uri object that receives the result of the insertion lateinit var newUri: Uri ... // Defines an object to contain the new values to insert val newValues = ContentValues().apply { /* * Sets the values of each column and inserts the word. The arguments to the "put" * method are "column name" and "value". */ put(UserDictionary.Words.APP_ID, "example.user") put(UserDictionary.Words.LOCALE, "en_US") put(UserDictionary.Words.WORD, "insert") put(UserDictionary.Words.FREQUENCY, "100") } newUri = contentResolver.insert( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI newValues // The values to insert )
Java
// Defines a new Uri object that receives the result of the insertion Uri newUri; ... // Defines an object to contain the new values to insert ContentValues newValues = new ContentValues(); /* * Sets the values of each column and inserts the word. The arguments to the "put" * method are "column name" and "value". */ newValues.put(UserDictionary.Words.APP_ID, "example.user"); newValues.put(UserDictionary.Words.LOCALE, "en_US"); newValues.put(UserDictionary.Words.WORD, "insert"); newValues.put(UserDictionary.Words.FREQUENCY, "100"); newUri = getContentResolver().insert( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI newValues // The values to insert );
הנתונים של השורה החדשה מועברים לאובייקט ContentValues
יחיד, שדומה בצורתו לסמן בשורה אחת. העמודות באובייקט הזה לא חייבות להיות מאותו סוג נתונים, ואם אתם לא רוצים לציין ערך בכלל, תוכלו להגדיר עמודה לערך null
באמצעות ContentValues.putNull()
.
קטע הקוד הקודם לא מוסיף את העמודה _ID
, כי העמודה הזו מתוחזקת באופן אוטומטי. הספק מקצה ערך ייחודי של _ID
לכל שורה שמתווספת. ספקים בדרך כלל משתמשים בערך הזה כמפתח הראשי של הטבלה.
ה-URI של התוכן שמוחזר ב-newUri
מזהה את השורה שנוספה החדשה בפורמט הבא:
content://user_dictionary/words/<id_value>
הערך של <id_value>
הוא התוכן של _ID
בשורה החדשה.
רוב הספקים יכולים לזה��ת ��ת ה��ורה ��זו ��ל URI של ��וכן באופן אוטומטי ולאחר מכן לבצע את הפעולה המבוקשת בשורה המסוימת הזו.
כדי לקבל את הערך של _ID
מה-Uri
שהוחזר, צריך להפעיל את ContentUris.parseId()
.
עדכון נתונים
כדי לעדכן שורה, משתמשים באובייקט ContentValues
עם הערכים המעודכנים, בדיוק כמו שמשתמשים באובייקט כזה להוספה, ועם קריטריונים לבחירה, בדיוק כמו שמשתמשים בהם בשאילתה.
שיטת הלקוח שבה אתם משתמשים היא ContentResolver.update()
. צריך להוסיף ערכים לאובייקט ContentValues
רק לעמודות שאתם מעדכנים. כדי לנקות את התוכן של עמודה, מגדירים את הערך כ-null
.
קטע הקוד הבא משנה את כל השורות שבלוקאל שלהן מוגדרת השפה "en"
ללוקאל null
. הערך המוחזר הוא מספר השורות שעודכנו.
Kotlin
// Defines an object to contain the updated values val updateValues = ContentValues().apply { /* * Sets the updated value and updates the selected words. */ putNull(UserDictionary.Words.LOCALE) } // Defines selection criteria for the rows you want to update val selectionClause: String = UserDictionary.Words.LOCALE + "LIKE ?" val selectionArgs: Array<String> = arrayOf("en_%") // Defines a variable to contain the number of updated rows var rowsUpdated: Int = 0 ... rowsUpdated = contentResolver.update( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI updateValues, // The columns to update selectionClause, // The column to select on selectionArgs // The value to compare to )
Java
// Defines an object to contain the updated values ContentValues updateValues = new ContentValues(); // Defines selection criteria for the rows you want to update String selectionClause = UserDictionary.Words.LOCALE + " LIKE ?"; String[] selectionArgs = {"en_%"}; // Defines a variable to contain the number of updated rows int rowsUpdated = 0; ... /* * Sets the updated value and updates the selected words. */ updateValues.putNull(UserDictionary.Words.LOCALE); rowsUpdated = getContentResolver().update( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI updateValues, // The columns to update selectionClause, // The column to select on selectionArgs // The value to compare to );
אני רוצה למחוק את הקלט של המשתמשים כשמתקשרים אל
ContentResolver.update()
. מידע נוסף מופיע בקטע הגנה מפני קלט זדוני.
מחיקת נתונים
מחיקת שורות דומה לאחזור נתוני שורות. מציינים קריטריונים לבחירת השורות שרוצים למחוק, ושיטת הלקוח מחזירה את מספר השורות שנמחקו.
קטע הקוד הבא ממחק שורות שמזהה האפליקציה שלהן תואם ל-"user"
. ה-method מחזירה את מספר השורות שנמחקו.
Kotlin
// Defines selection criteria for the rows you want to delete val selectionClause = "${UserDictionary.Words.APP_ID} LIKE ?" val selectionArgs: Array<String> = arrayOf("user") // Defines a variable to contain the number of rows deleted var rowsDeleted: Int = 0 ... // Deletes the words that match the selection criteria rowsDeleted = contentResolver.delete( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI selectionClause, // The column to select on selectionArgs // The value to compare to )
Java
// Defines selection criteria for the rows you want to delete String selectionClause = UserDictionary.Words.APP_ID + " LIKE ?"; String[] selectionArgs = {"user"}; // Defines a variable to contain the number of rows deleted int rowsDeleted = 0; ... // Deletes the words that match the selection criteria rowsDeleted = getContentResolver().delete( UserDictionary.Words.CONTENT_URI, // The UserDictionary content URI selectionClause, // The column to select on selectionArgs // The value to compare to );
צריך לנקות את הקלט של המשתמש כשקוראים ל-ContentResolver.delete()
. מידע נוסף זמין בקטע הגנה מפני קלט זדוני.
סוגי הנתונים של הספק
ספקי תוכן יכולים להציע סוגים רבים ושונים של נתונים. ספק המילון של המשתמש מציע רק טקסט, אבל הספקים יכולים להציע גם את הפורמטים הבאים:
- מספר שלם
- מספר שלם ארוך (long)
- נקודה צפה (floating-point)
- נקודה צפה ארוכה (double)
סוג נתונים נוסף שספקים משתמשים בו בדרך כלל הוא אובייקט גדול בינארי (BLOB) שמוטמע כמערך בייטים בגודל 64KB. כדי לראות את סוגי הנתונים הזמינים, אפשר לעיין
בשיטות "get" של מחלקה Cursor
.
סוג הנתונים של כל עמודה אצל ספק מסוים מופיע בדרך כלל במסמכי התיעוד שלו.
סוגי הנתונים של ספק מילון המשתמשים מפורטים במסמכי העזרה של סוג החוזה שלו, UserDictionary.Words
. מחלקות של ערכים קבוע��ם מתוארות בקטע מחלקות של ערכים קבועים.
אפשר גם לקבוע את סוג הנתונים באמצעות קריאה ל-Cursor.getType()
.
הספקים גם שומרים מידע על סוג הנתונים של MIME לכל URI של תוכן שהם מגדירים. אפשר להשתמש במידע על סוג ה-MIME כדי לבדוק אם האפליקציה יכולה לטפל בנתונים שהספק מציע, או כדי לבחור סוג טיפול על סמך סוג ה-MIME. בדרך כלל צריך את סוג ה-MIME כשעובדים עם ספק שמכיל מבנים מורכבים של נתונים או קבצים.
לדוגמה, בטבלה ContactsContract.Data
של ספק אנשי הקשר נעשה שימוש בסוגי MIME כדי לתייג את סוג נתוני אנשי הקשר שמאוחסנים בכל שורה. כדי לקבל את סוג ה-MIME התואם ל-URI של תוכן, צריך להפעיל את הפונקציה ContentResolver.getType()
.
בקטע חומר עזר בנושא סוגי MIME מוסבר התחביר של סוגי MIME רגילים ומותאמים אישית.
דרכים חלופיות לגישה של ספקים
יש שלוש דרכים חלופיות לגישה של ספקים לפיתוח אפליקציות:
-
גישה באצווה: אפשר ליצור אצווה של קריאות גישה עם methods במחלקה
ContentProviderOperation
, ואז להחיל אותן עםContentResolver.applyBatch()
. -
שאילתות אסינכרניות: ביצוע שאילתות בשרשור נפרד. אפשר להשתמש באובייקט
CursorLoader
. הדוגמאות במדריך Loaders מדגימות איך לעשות את זה. - גישה לנתונים באמצעות כוונות: אי אפשר לשלוח כוונה ישירות לספק, אבל אפשר לשלוח כוונה לאפליקציה של הספק, שבדרך כלל היא הכלי המתאים ביותר לשינוי הנתונים של הספק.
הגישה והשינוי של קבוצות של פריטים באמצעות כוונות מתוארים בקטעים הבאים.
גישה לאצווה
גישה באצווה לספק שימושית להוספה של מספר גדול של שורות, להוספת שורות במספר טבלאות באותה קריאה ל-method, ובאופן כללי לביצוע קבוצה של פעולות מעבר לגבולות התהליך כטרנזקציה, שנקראת פעולה אטומית.
כדי לגשת לספק במצב אצווה, צריך ליצור מערך של אובייקטים מסוג ContentProviderOperation
ולשלוח אותם לספק תוכן באמצעות ContentResolver.applyBatch()
. מעבירים למתודה הזו את הסמכות של ספק התוכן, ולא URI ספציפי של תוכן.
כך כל אובייקט ContentProviderOperation
במערך יכול לפעול בטבלה אחרת. קריאה ל-ContentResolver.applyBatch()
מחזירה מערך של תוצאות.
התיאור של סוג החוזה ContactsContract.RawContacts
כולל קטע קוד שממחיש הוספה בכמות גדולה.
גישה לנתונים באמצעות כוונות
כוונות יכולות לספק גישה עקיפה לספק תוכן. אתם יכולים לאפשר למשתמש לגשת לנתונים של ספק גם אם לאפליקציה שלכם אין הרשאות גישה. לשם כך, אתם יכולים לקבל חזרה כוונה (intent) עם תוצאה מאפליקציה שיש לה הרשאות, או להפעיל אפליקציה שיש לה הרשאות ולאפשר למשתמש לבצע בה פעולות.
קבלת גישה עם הרשאות זמניות
אפשר לגשת לנתונים בספקי תוכן גם אם אין לכם את הרשאות הגישה המתאימות, על ידי שליחת כוונה (intent) לאפליקציה שיש לה את ההרשאות, וקבלת חזרה כוונה (intent) עם הרשאות URI. אלה הרשאות ל-URI ספציפי של תוכן, שתוקפן נמשך עד לסיום הפעילות שמקבלת אותן. האפליקציה עם ההרשאות הקבועות מעניקה הרשאות זמניות על ידי הגדרת דגל ב-Intent של התוצאה:
-
הרשאת קריאה:
FLAG_GRANT_READ_URI_PERMISSION
-
הרשאת כתיבה:
FLAG_GRANT_WRITE_URI_PERMISSION
הערה: הדגלים האלה לא מעניקים גישה כללית לקריאה או לכתיבה לספק שהסמכות שלו כלולות ב-URI של התוכן. הגישה היא רק ל-URI עצמו.
כששולחים מזהי URI של תוכן לאפליקציה אחרת, צריך לכלול לפחות אחד מהדגלים האלה. הדגלים מספקים את היכולות הבאות לכל אפליקציה שמקבלת Intent ומטרגטת ל-Android 11 (רמת API 30) ואילך:
- קריאה מהנתונים ש-URI התוכן מייצג או כתיבת אליהם, בהתאם לדגל שכלול בכוונה.
- מקבלים הרשאות גישה של חבילות לאפליקציה שמכילה את ספק התוכן שתואם לרשות ה-URI. יכול להיות שהאפליקציה ששולחת את הכוונה והאפליקציה שמכילה את ספ�� התוכן הן שתי אפליקציות שונות.
הספק מגדיר הרשאות URI למזהי URI של תוכן במניפסט שלו, באמצעות המאפיין android:grantUriPermissions
של הרכיב <provider>
וגם באמצעות רכיב הצאצא <grant-uri-permission>
של הרכיב <provider>
. מנגנון ההרשאות של URI מוסבר בפירוט רב יותר במדריך הרשאות ב-Android.
לדוגמה, אפשר לאחזר נתונים של איש קשר ב-Contacts Provider גם אם אין ��כם את ההרשאה READ_CONTACTS
. כדאי לעשות זאת באפליקציה ששולחת ברכות באימייל לאיש קשר ביום ההולדת שלו. במקום לבקש את
READ_CONTACTS
, שמעניק לך גישה לכל
אנשי הקשר של המשתמש ולכל המידע שלו, אפשר למשתמש להחליט באילו אנשי קשר האפליקציה תשתמש. כדי לעשות זאת, פועלים לפי התהליך הבא:
-
באפליקציה, שולחים Intent שמכיל את הפעולה
ACTION_PICK
ואת סוג ה-MIME 'אנשי קשר'CONTENT_ITEM_TYPE
, באמצעות השיטהstartActivityForResult()
. - מכיוון שהכוונה הזו תואמת למסנן הכוונה של הפעילות 'בחירה' באפליקציית 'אנשים', הפעילות הזו עוברת לחזית.
-
בפעילות הבחירה, המשתמש בוחר
איש קשר לעדכון. במקרים כאלה, פעילות הבחירה מפעילה
setResult(resultcode, intent)
כדי להגדיר כוונה להחזיר לאפליקציה שלך. ה-intent מכיל את ה-URI של התוכן של איש הקשר שבחר המשתמש ואת הדגליםFLAG_GRANT_READ_URI_PERMISSION
של 'תוספים'. הדגלים האלה מעניקים לאפליקציה הרשאת URI לקרוא נתונים של איש הקשר שמפנים אליו באמצעות מזהה ה-URI של התוכן. לאחר מכן, פעילות הבחירה קוראת ל-finish()
כדי להחזיר את השליטה לאפליקציה. -
הפעילות חוזרת לחזית והמערכת קוראת לשיטה
onActivityResult()
של הפעילות. השיטה הזו מקבלת את כוונת התוצאה שנוצרה על ידי פעילות הבחירה באפליקציית 'אנשים'. - בעזרת ה-URI של התוכן ב-Intent של התוצאה, אפשר לקרוא את הנתונים של איש הקשר מספק אנשי הקשר, גם אם לא ביקשת לקבל הרשאת גישה קבועה לקריאה במניפסט של הספק. אחרי זה אפשר לקבל את פרטי יום ההולדת או כתובת האימייל של איש הקשר.
שימוש באפליקציה אחרת
דרך נוספת לאפשר למשתמש לשנות נתונים שאין לכם הרשאות גישה אליהם היא להפעיל אפליקציה שיש לה הרשאות ולאפשר למשתמש לבצע את העבודה שם.
לדוגמה, האפליקציה של יומן Google מקבלת Intent מסוג ACTION_INSERT
שמאפשר להפעיל את ממשק המשתמש של ההוספה. אפשר להעביר נתונים 'נוספים' בכוונה הזו, והאפליקציה משתמשת בהם כדי לאכלס מראש את ממשק המשתמש. לאירועים חוזרים יש תחביר מורכב, ולכן הדרך המועדפת להוספת אירועים לספק יומן Google היא להפעיל את אפליקציית יומן Google באמצעות ACTION_INSERT
ולאחר מכן לאפשר למשתמש להוסיף את האירוע שם.
הצגת נתונים באמצעות אפליקציה מסייעת
אם לאפליקציה שלכם יש הרשאות גישה, יכול להיות שעדיין משתמשים בכוונה להציג נתונים באפליקציה אחרת. לדוגמה, אפליקצי��ת יומן Google מקבלת כוונה מסוג ACTION_VIEW
שמציגה תאריך או אירוע מסוימים.
כך תוכלו להציג את פרטי היומן בלי ליצור ממשק משתמש משלכם.
מידע נוסף על התכונה הזו זמין במאמר סקירה כללית על ספקי יומנים.
האפליקציה שאליה שולחים את הכוונה לא חייבת להיות האפליקציה המשויכת לספק. לדוגמה, אפשר לאחזר איש קשר מספק אנשי הקשר, ואז לשלוח למציג התמונות כוונה מסוג ACTION_VIEW
שמכילה את URI התוכן של התמונה של איש הקשר.
מחלקות של ערכים קבועים (contract class)
בכיתה של חוזה מוגדרים קבועים שעוזרים לאפליקציות לעבוד עם מזהי ה-URI של התוכן, שמות העמודות, פעולות הכוונה ותכונות אחרות של ספק התוכן. כיתות בחוזה לא נכללות באופן אוטומטי בספקי השירות. המפתח של הספק צריך להגדיר אותם ואז להפוך אותם לזמינים למפתחים אחרים. לרבים מהספקים שכלולים בפלטפורמת Android יש שיעורי חוזים תואמים בחבילה android.provider
.
לדוגמה, לספק של מילון המשתמשים יש מחלקת חוזה UserDictionary
שמכילה ערכי קבוע של כתובות URI של תוכן ושמות עמודות. ה-URI של התוכן של הטבלה Words
מוגדר
בקבוע UserDictionary.Words.CONTENT_URI
.
הכיתה UserDictionary.Words
מכילה גם קבועים של שמות עמודות, שמשמשים בקטעי הקוד לדוגמה במדריך הזה. לדוגמה, אפשר להגדיר הקרנה של שאילתה באופן הבא:
Kotlin
val projection : Array<String> = arrayOf( UserDictionary.Words._ID, UserDictionary.Words.WORD, UserDictionary.Words.LOCALE )
Java
String[] projection = { UserDictionary.Words._ID, UserDictionary.Words.WORD, UserDictionary.Words.LOCALE };
סוג חוזה נוסף הוא ContactsContract
עבור ספק אנשי הקשר.
במסמכי העזרה של המחלקה הזו יש קטעי קוד לדוגמה. אחת מהתת-כיתות שלו, ContactsContract.Intents.Insert
, היא כיתת חוזה שמכילה קבועים לכוונות ולנתוני הכוונה.
הפנייה לסוג MIME
ספקי תוכן יכולים להחזיר סוגי מדיה רגילים של MIME, מחרוזות של סוגי MIME מותאמים אישית או את שניהם.
סוגי ה-MIME הם בפורמט הבא:
type/subtype
לדוגמה, סוג ה-MIME המוכר text/html
כולל את הסוג text
ואת סוג המשנה html
. אם הספק מחזיר את הסוג הזה לכתובת URI, המשמעות היא ששאי��תה שמשתמשת בכתובת ה-URI הזו מחזירה טקסט שמכיל תגי HTML.
למחרוזות של סוגי MIME מותאמים אישית, שנקראות גם סוגי MIME ספציפיים לספק, יש ערכים מורכבים יותר של type ו-subtype. אם ��ש לכם כמה שורות, ערך הסוג תמיד יהיה:
vnd.android.cursor.dir
בשורה אחת, ערך הסוג הוא תמיד:
vnd.android.cursor.item
הקובץ subtype הוא ספציפי לספק. בדרך כלל, לספקים המובנים של Android יש תת-סוג פשוט. לדוגמה, כשאפליקציית אנשי הקשר יוצרת שורה למספר טלפון, היא מגדירה את סוג ה-MIME הבא בשורה:
vnd.android.cursor.item/phone_v2
הערך של סוג המשנה הוא phone_v2
.
מפתחים אחרים של ספקים יכולים ליצור דפוס משלהם של תת-סוגים על סמך שמות הרשות והטבלה של הספק. לדוגמה, ספק שמכיל לוחות זמנים של רכבות.
הרשאת הספק של הספק היא com.example.trains
, והיא מכילה את הטבלאות
Line1, Line2 ו-Line3. בתגובה ל-URI התוכן הבא של טבלה Line1:
content://com.example.trains/Line1
הספק מחזיר את סוג ה-MIME הבא:
vnd.android.cursor.dir/vnd.example.line1
בתגובה ל-URI הבא של התוכן בשורה 5 בטבלה Line2:
content://com.example.trains/Line2/5
הספק מחזיר את סוג ה-MIME הבא:
vnd.android.cursor.item/vnd.example.line2
רוב ספקי התוכן מגדירים קבועים של מחלקות חוזים לסוגי ה-MIME שבהם הם משתמשים. לדוגמה, סוג החוזה של ספק אנשי הקשר ContactsContract.RawContacts
מגדיר את הקבוע CONTENT_ITEM_TYPE
לסוג MIME של שורה גולמית אחת של איש קשר.
מזהי URI של תוכן לשורות בודדות מתוארים בקטע URI של תוכן.