ספק תוכן מנהל את הגישה למאגר נתונים מרכזי. ספק הוא חלק מאפליקציה ל-Android, שלרוב מספקת ממשק משתמש משלו לעבודה עם של הנתונים. אבל ספקי תוכן משמשים בעיקר חברות אחרות ניגשות לספק באמצעות אובייקט לקוח של ספק. יחד, ספקים והספקים שלהם מציעים ממשק סטנדרטי ועקבי לנתונים שמטפל גם תקשורת בין תהליכים וגישה מאובטחת לנתונים.
בדרך כלל עובדים עם ספקי תוכן באחד משני תרחישים: כדי לגשת לספק תוכן קיים באפליקציה אחרת או ספק תוכן חדש באפליקציה כדי לשתף נתונים עם אפליקציות אחרות.
הדף הזה עוסק בעקרונות הבסיסיים של העבודה עם ספקי תוכן קיימים. מידע נוסף על הטמעה ספקי תוכן באפליקציות שלכם, יוצרים ספק תוכן.
נושא זה מתאר את הנושאים הבאים:
- הסבר על ספקי תוכן
- ה-API שבו אתם משתמשים כדי לאחזר נתונים מספק תוכן.
- ממשק ה-API שבו אתם משתמשים כדי להוסיף, לעדכן או למחוק נתונים בספק התוכן.
- תכונות API נוספות שעוזרות לעבוד עם ספקים.
סקירה כללית
ספק תוכן מציג נתונים לאפליקציות חיצוניות כטבלה אחת או יותר בדומה לטבלאות שבמסד נתונים רלציוני. שורה מייצגת מופע מסוג כלשהו של נתונים שהספק אוסף, וכל עמודה בשורה מייצגת חלק שנאספו עבור מופע מסוים.
ספק תוכן מתאם את הגישה לשכבת אחסון הנתונים באפליקציה כדי מספר ממשקי ה-API והרכיבים השונים. כפי שמתואר באיור 1, הכללים האלה כוללים:
- שיתוף גישה לנתוני האפליקציה עם אפליקציות אחרות
- שליחת נתונים לווידג'ט
- החזרת הצעות חיפוש מותאמות אישית עבור האפליקציה שלך דרך החיפוש
framework באמצעות
SearchRecentSuggestionsProvider
- סנכרון נתוני אפליקציה עם השרת שלכם באמצעות יישום של
AbstractThreadedSyncAdapter
- טעינת נתונים בממשק המשתמש באמצעות
CursorLoader
![הקשר בין ספק התוכן לבין רכיבים אחרים.](https://developer.android.com/static/guide/topics/providers/images/content-provider-tech-stack.png?authuser=9&hl=he)
איור 1. הקשר בין ספק תוכן לבין רכיבים אחרים.
גישה לספק
כדי לגשת לנתונים בספק תוכן, צריך להשתמש
אובייקט אחד (ContentResolver
) של האפליקציה
Context
כדי לתקשר עם הספק כלקוח.
אובייקט ContentResolver
מתקשר עם אובייקט הספק,
מופע של מחלקה שמממשת את ContentProvider
.
הספק
האובייקט מקבל בקשות לנתונים מלקוחות, מבצע את הפעולה המבוקשת ומחזיר
תוצאות. לאובייקט הזה יש methods שקוראות לשיטות עם שם זהה באובייקט הספק,
מכונה של אחת ממחלקות המשנה הממשיות של ContentProvider
.
השיטות של ContentResolver
מספקות את רמת הגישה הבסיסית
'CRUD' פונקציות (יצירה, אחזור, עדכון ומחיקה) של אחסון מתמיד.
דפוס נפוץ של גישה אל ContentProvider
מממשק המשתמש שלך משתמש
CursorLoader
כדי להריץ שאילתה אסינכרונית ברקע.
הפקודה Activity
או Fragment
בממשק המשתמש קוראות לפונקציה
CursorLoader
לשאילתה, וכתוצאה מכך מקבלת את
ContentProvider
באמצעות ContentResolver
.
כך ממשק המשתמש ימשיך להיות זמין למשתמש בזמן שהשאילתה פועלת. הזה כוללת אינטראקציה בין מספר אובייקטים שונים, וגם מנגנון אחסון, כמו שמתואר באיור 2.
![אינטראקציה בין ContentProvider, מחלקות אחרות ואחסון.](https://developer.android.com/static/guide/topics/providers/images/content-provider-interaction.png?authuser=9&hl=he)
איור 2. אינטראקציה בין ContentProvider
, מחלקות אחרות ואחסון.
הערה: כדי לגשת לספק, האפליקציה בדרך כלל צריכה לבקש בקובץ המניפסט. דפוס התפתחות זה מתואר בפירוט הקטע הרשאות של ספקי תוכן.
אחד מהספקים המובנים בפלטפורמת Android הוא User מילון Provider, שומרת את המילים הלא סטנדרטיות שהמשתמש רוצה לשמור. טבלה 1 ממחישה את מה הנתונים עשויים להיראות בטבלה של הספק הזה:
טבלה 1: טבלה לדוגמה של מילון משתמש.
מילים | מזהה אפליקציה | תדירות | שילוב של שפה ואזור | _ID |
---|---|---|---|---|
mapreduce |
משתמש1 | 100 | iw_IL | 1 |
precompiler |
משתמש14 | 200 | fr_FR | 2 |
applet |
משתמש2 | 225 | fr_CA | 3 |
const |
משתמש1 | 255 | נק'_BR | 4 |
int |
משתמש5 | 100 | iw_IL | 5 |
בטבלה 1, כל שורה מייצגת מופע של מילה שאינה
שנמצא במילון רגיל. כל עמודה מייצגת חלק של נתונים לאותה מילה, כמו
הלוקאל שבו המערכת נתקלה לראשונה. הכותרות של העמודות הן שמות של עמודות ששמורות באחסון
הספק. למשל, כדי להתייחס ללוקאל של שורה מסוימת, צריך לעיין בעמודה locale
שלה. עבור
הספק הזה, העמודה _ID
משמשת כעמודת מפתח ראשי
שמספק הספק באופן אוטומטי.
כדי לקבל רשימה של המילים והלוקאלים שלהן מספק המילון של המשתמש,
התקשרות אל ContentResolver.query()
.
השיטה query()
קוראת ל-
השיטה 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)
מתאימים למשפט SQL SELECT:
טבלה 2: query()
בהשוואה לשאילתת SQL.
ארגומנט אחד (query() ) |
SELECT מילת מפתח/פרמטר | הערות |
---|---|---|
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.
לדוגמה, המשתמש יכול להזין 'שום דבר; 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
מספק גישת קריאה אקראית לשורות ולעמודות שהוא כולל
מכיל/ה.
באמצעות methods של Cursor
, אפשר לחזור על השורות של
תוצאות, לקבוע את סוג הנתונים של כל עמודה, להוציא את הנתונים מעמודה ולבחון
של התוצאות.
חלק מההטמעות של Cursor
באופן אוטומטי
עדכון האובייקט כשהנתונים של הספק משתנים, הפעלת methods באובייקט של צופה
כאשר 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()
שמחזירה ערך שמציין
סוג הנתונים של העמודה.
הרשאות של ספקי תוכן
אפליקציה של ספק יכולה לציין הרשאות שאפליקציות אחרות חייבות לקבל לגשת לנתוני הספק. ההרשאות האלה מאפשרות למשתמש לדעת אילו נתונים אפליקציה מנסה לגשת. בהתאם לדרישות הספק, אפליקציות אחרות לבקש את ההרשאות הדרושות כדי לגשת לספק. משתמשי הקצה רואים את הרשאות בזמן ההתקנה של האפליקציה.
אם אפליקציה של ספק לא מציינת הרשאות, אז לאפליקציות אחרות אין גישה לנתונים של הספק, אלא אם הספק מיוצא. בנוסף, רכיבים באפליקציה של הספק תמיד תהיה גישת קריאה וכתיבה מלאה, בלי קשר ההרשאות שצוינו.
ספק המילון של המשתמש דורש את
הרשאה ל-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
של הספק כדי לשנות נתונים.
קוראים לשיטה של ContentResolver
עם ארגומנטים שמועברים אל
את ה-method המתאים של ContentProvider
. הספק והספק
הלקוח יטפל אוטומטית באבטחה ובתקשורת בין תהליכים.
הוספת נתונים
כדי להוסיף נתונים לספק, קוראים
ContentResolver.insert()
. השיטה הזו מכניסה שורה חדשה לספק ומחזירה URI של תוכן עבור השורה הזו.
קטע הקוד הבא מראה איך להוסיף מילה חדשה ב-User Introduction Provider:
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()
מידע נוסף על
במקרה כזה, אפשר לקרוא את הקטע הגנה מפני קלט זדוני.
סוגי נתונים של ספקים
ספקי תוכן יכולים להציע סוגים רבים ושונים של נתונים. ספק המילון של המשתמש מציע רק אבל ספקים יכולים להציע גם את הפורמטים הבאים:
- מספר שלם
- מספר שלם ארוך (ארוך)
- נקודה צפה (floating-point)
- נקודה צפה ארוכה (כפול)
סוג נתונים נוסף שספקים משתמשים בו לעיתים קרובות הוא אובייקט גדול בינארי (BLOB) שמוטמע בתור
מערך בגודל 64KB. כדי לראות את סוגי הנתונים הזמינים, אפשר לעיין
מחלקה אחת (Cursor
) מסוג 'get' שיטות.
סוג הנתונים של כל עמודה בספק מופיע בדרך כלל במסמכים הרלוונטיים.
סוגי הנתונים של ספק המילון למשתמש מפורטים במסמכי העזרה
לסיווג החוזה שלו, UserDictionary.Words
. סוגי החוזים הם
שמתוארים בקטע סוגי חוזים.
אפשר גם לקבוע את סוג הנתונים באמצעות קריאה ל-Cursor.getType()
.
הספקים גם שומרים על פרטי סוג נתוני MIME עבור כל URI של תוכן שהם מגדירים. אפשר להשתמש במידע מסוג MIME כדי לבדוק אם האפליקציה יכולה לטפל בנתונים ההצעות של הספק או לבחור סוג טיפול לפי סוג MIME. בדרך כלל נדרשות סוג MIME כשעובדים עם ספק שמכיל של מבני נתונים או של קבצים.
לדוגמה, ContactsContract.Data
הטבלה ב'ספק אנשי הקשר' משתמשת בסוגי MIME כדי להוסיף תוויות לסוג הנתונים של אנשי הקשר שמאוחסנים בכל
השורה הראשונה. כדי לקבל את סוג ה-MIME המתאים ל-URI של תוכן, יש להפעיל
ContentResolver.getType()
בקטע הפניית סוג MIME מתאר את של סוגי MIME רגילים וגם מותאמים אישית.
צורות חלופיות של גישה לספקים
יש שלוש צורות חלופיות של גישה לספקים שחשובות בפיתוח אפליקציות:
-
גישה באצווה: אפשר ליצור אצווה של קריאות גישה באמצעות methods
את המחלקה
ContentProviderOperation
ואז להחיל אותם עםContentResolver.applyBatch()
-
שאילתות אסינכרוניות: ביצוע שאילתות בשרשור נפרד. אפשר
משתמשים באובייקט
CursorLoader
. הדוגמאות הדגמה של מדריך מטענים איך לעשות את זה. - גישה לנתונים באמצעות כוונות: אין לכם אפשרות לשלוח Intent ישירות לספק, אפשר לשלוח Intent לאפליקציה של הספק, את הציוד המתאים ביותר בדרך כלל לשינוי הנתונים של הספק.
בקטעים הבאים מתוארים בקטעים הבאים גישה בכמות גדולה ושינוי שלהם באמצעות כוונות.
גישה בכמות גדולה
אפשר להשתמש בגישה בכמות גדולה לספק כדי להוסיף מספר גדול של שורות שורות במספר טבלאות באותה קריאה ל-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 של תוכן לאפליקציה אחרת, צריך לכלול לפחות אחד מהם סימונים אוטומטיים. הסימונים מספקים את היכולות הבאות לכל אפליקציה שמקבלת גישה כוונה ומטרגטת ל-Android 11 (רמת API 30) ואילך:
- קריאה מהנתונים שה-URI של התוכן מייצג בהם או כתיבה אליהם, בהתאם לדגל שכלול ב-Intent.
- קבלת חבילה חשיפה לאפליקציה שמכילה את ספק התוכן שתואם רשות URI. האפליקציה ששולחת את הכוונה והאפליקציה ששלחה את שכולל את ספק התוכן יכול להיות שתי אפליקציות שונות.
ספק מגדיר הרשאות URI למזהי URI של תוכן במניפסט שלו, באמצעות
android:grantUriPermissions
של התכונה
<provider>
וגם את
<grant-uri-permission>
רכיב הצאצא של
<provider>
לרכיב מסוים. מנגנון ההרשאות של URI מוסבר בפירוט
מדריך בנושא הרשאות ב-Android.
לדוגמה, תוכל לאחזר נתונים של איש קשר ב'ספק אנשי הקשר', גם אם לא
יש את ההרשאה READ_CONTACTS
. מומלץ לבצע
זאת באפליקציה ששולחת פתיחות אלקטרוניות לאיש קשר ביום ההולדת שלו. במקום
שליחת בקשה ל-READ_CONTACTS
, שמעניקה לך גישה לכל
אנשי הקשר וכל המידע של המשתמש, המשתמשים יכולים לקבוע
אנשי הקשר שבהם האפליקציה משתמשת. לשם כך, פועלים לפי התהליך הבא:
-
באפליקציה שלך, עליך לשלוח Intent שמכיל את הפעולה
ACTION_PICK
ו"אנשי הקשר" סוג MIMECONTENT_ITEM_TYPE
, באמצעות השיטהstartActivityForResult()
. - כי Intent זה תואם למסנן Intent של 'הבחירה' של אפליקציית אנשים פעילות, הפעילות מגיעה לחזית.
-
בפעילות הבחירה, המשתמש בוחר
איש קשר לעדכון. במקרה כזה, הפעילות של הבחירה מופעלת
setResult(resultcode, intent)
כדי להגדיר Intent להשיב לאפליקציה שלך. ה-Intent מכיל את ה-URI של התוכן של איש הקשר שהמשתמש בחר ושל ה'תוספות' דגליםFLAG_GRANT_READ_URI_PERMISSION
הדגלים האלה מעניקים URI הרשאה לאפליקציה לקרוא נתונים של איש הקשר שאליו מפנה המשתמש ב-URI של התוכן. פעילות הבחירה קוראת לאחר מכן ל-finish()
אל להחזיר שליטה באפליקציה. -
הפעילות חוזרת לקדמת התמונה והמערכת קוראת לפעילות
onActivityResult()
. השיטה הזו מקבלת את Intent התוצאה שנוצרה על ידי פעילות הבחירה ב- אפליקציית 'אנשים'. - ניתן לקרוא את הנתונים של איש הקשר בעזרת ה-URI של התוכן מתוך הכוונה של התוצאה מספק אנשי הקשר, למרות שלא ביקשת הרשאת קריאה קבועה לספק במניפסט. אפשר לקבל את פרטי יום ההולדת של איש הקשר או כתובת אימייל, ואז שולחים את הודעת הפתיחה.
שימוש באפליקציה אחרת
דרך נוספת לאפשר למשתמש לשנות נתונים שאין לך הרשאות גישה אליהם היא להפעיל אפליקציה שיש לה הרשאות ולאפשר למשתמש לבצע את העבודה שם.
לדוגמה, האפליקציה של יומן Google מקבלת
Intent מסוג ACTION_INSERT
שמאפשר להפעיל את
של ממשק המשתמש להוספה של האפליקציה. ניתן להעביר 'תוספות' ב-Intent הזה,
משמש לאכלוס מראש של ממשק המשתמש. מכיוון שלאירועים חוזרים יש תחביר מורכב, לכן העדיפות
להוסיף אירועים ל'ספק היומן' היא להפעיל את אפליקציית היומן עם
ACTION_INSERT
ואז המשתמש יכול להוסיף שם את האירוע.
הצגת נתונים באמצעות אפליקציית עזרה
אם לאפליקציה שלך יש הרשאות גישה, עדיין אפשר להשתמש
כוונה להציג נתונים באפליקציה אחרת. לדוגמה, האפליקציה של יומן Google מקבלת
Intent של ACTION_VIEW
שמציג תאריך או אירוע מסוים.
כך תוכלו להציג את פרטי היומן בלי ליצור ממשק משתמש משלכם.
מידע נוסף על התכונה הזו זמין במאמר
סקירה כללית של ספק היומן
האפליקציה שאליה אתם שולחים את הכוונה לא חייבת להיות האפליקציה
המשויך לספק. לדוגמה, ניתן לאחזר איש קשר
צריך לפנות לספק ואז לשלוח Intent מסוג ACTION_VIEW
שמכיל את ה-URI של התוכן לתמונה של איש הקשר למציג התמונות.
סוגי חוזים
מחלקה של חוזה מגדירה קבועים שעוזרים לאפליקציות לעבוד עם מזהי ה-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 הבא של התוכן עבור שורה 1 בטבלה:
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 של תוכן.