מידע בסיסי על ספקי תוכן

ספק התוכן מנהל את הגישה למאגר מרכזי של נתונים. ספק הוא חלק מאפליקציה ל-Android, שלרוב מספקת ממשק משתמש משלו לעבודה עם הנתונים. עם זאת, ספקי התוכן משמשים בעיקר אפליקציות אחרות, שמקבלות גישה לספק באמצעות אובייקט לקוח של ספק. יחד, הספקים ולקוחות הספקים מציעים ממשק סטנדרטי ועקבי לנתונים שמטפל גם בתקשורת בין תהליכים ובגישה מאובטחת לנתונים.

בדרך כלל עובדים עם ספקי תוכן באחד משני התרחישים הבאים: הטמעת קוד כדי לגשת לספק תוכן קיים באפליקציה אחרת, או יצירת ספק תוכן חדש באפליקציה כדי לשתף נתונים עם אפליקציות אחרות.

בדף הזה נסביר את העקרונות הבסיסיים לעבודה עם ספקי תוכן קיימים. למידע נוסף על הטמעת ספקי תוכן באפליקציות שלכם, קראו את המאמר יצירת ספק תוכן.

בנושא הזה נסביר את הנושאים הבאים:

  • איך פועלים ספקי התוכן.
  • ה-API שבו אתם משתמשים כדי לאחזר נתונים מספק תוכן.
  • ה-API שבו משתמשים כדי להוסיף, לעדכן או למחוק נתונים אצל ספק התוכן.
  • תכונות API נוספות שעוזרות לעבוד עם ספקים.

סקירה כללית

ספק תוכן מציג נתונים לאפליקציות חיצוניות כטבלה אחת או יותר, שדומות לטבלאות במסד נתונים יחסיים. שורה מייצגת מופע של סוג כלשהו של נתונים שהספק אוסף, וכל עמודה בשורה מייצגת נתון ספציפי שנאסף לגבי מופע.

ספק תוכן מתאם את הגישה לשכבת אחסון הנתונים באפליקציה עבור מספר ממשקי API ורכיבים שונים. כפי שמוצג באיור 1, אלה כוללים:

  • שיתוף הגישה לנתוני האפליקציה עם אפליקציות אחרות
  • שליחת נתונים לווידג'ט
  • הצגת הצעות חיפוש בהתאמה אישית לאפליקציה באמצעות מסגרת החיפוש באמצעות SearchRecentSuggestionsProvider
  • סנכרון נתוני האפליקציה עם השרת באמצעות הטמעה של AbstractThreadedSyncAdapter
  • טעינת נתונים בממשק המשתמש באמצעות CursorLoader
הקשר בין ספק התוכן לבין רכיבים אחרים.

איור 1. הקשר בין ספק התוכן לבין רכיבים אחרים.

גישה לספק

כדי לגשת לנתונים בספק תוכן, צריך להשתמש באובייקט ContentResolver ב-Context של האפליקציה כדי לתקשר עם הספק כלקוח. אובייקט ContentResolver מתקשר עם אובייקט הספק, שהוא מופע של כיתה שמטמיעה את ContentProvider.

אובייקט הספק מקבל בקשות נתונים מהלקוחות, מבצע את הפעולה המבוקשת ומחזיר את התוצאות. לאובייקט הזה יש methods שקוראות ל-methods עם שם זהה באובייקט הספק, מכונה של אחת ממחלקות המשנה הממשיות של ContentProvider. ה-methods ContentResolver מספקות את הפונקציות הבסיסיות 'CRUD' (יצירה, אחזור, עדכון ומחיקה) של אחסון מתמיד.

דפוס נפוץ לגישה ל-ContentProvider מממשק המשתמש הוא שימוש ב-CursorLoader כדי להריץ שאילתה אסינכרונית ברקע. הערך Activity או Fragment בממשק המשתמש קורא ל-CursorLoader בשאילתה, שמקבלת את הערך ContentProvider באמצעות ContentResolver.

כך ממשק המשתמש ימשיך להיות זמין למשתמש בזמן שהשאילתה פועלת. הדפוס הזה כולל אינטראקציה של מספר אובייקטים שונים, וגם מנגנון האחסון הבסיסי, כפי שמתואר באיור 2.

אינטראקציה בין ContentProvider, כיתות אחרות ואחסון.

איור 2. אינטראקציה בין ContentProvider, בין כיתות אחרות ובין אחסון.

הערה: כדי לגשת לספק, בדרך כלל האפליקציה צריכה לבקש הרשאות ספציפיות בקובץ המניפסט. דפוס ההתפתחות הזה מתואר בפירוט בקטע הרשאות של ספק תוכן.

אחד מהספקים המובנים בפלטפורמת Android הוא ספק מילון המשתמש, שמאחסן את המילים הלא סטנדרטיות שהמשתמש רוצה לשמור. בטבלה 1 מוסבר איך הנתונים עשויים להיראות בטבלה של הספק הזה:

טבלה 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:

טבלה 2: השוואה בין query() לשאילתת 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. בנוסף, שורות הקוד הן קטעי קוד בלבד. הן לא מציגות בקשה מלאה.

כדי לאחזר נתונים מספק מסוים, צריך לבצע את השלבים הבסיסיים הבאים:

  1. מבקשים הרשאת קריאה מהספק.
  2. מגדירים את הקוד ששולח שאילתה לספק.

שליחת בקשה להרשאת גישה לקריאה

כדי לאחזר נתונים מספק, לאפליקציה צריכה להיות הרשאת קריאה לספק. לא ניתן לבקש את ההרשאה הזו בזמן ריצה. במקום זאת, צריך לציין את הצורך בהרשאה הזו במניפסט באמצעות הרכיב <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 רגילים ומותאמים אישית.

דרכים חלופיות לגישה של ספקים

יש שלוש דרכים חלופיות לגישה של ספקים לפיתוח אפליקציות:

הגישה והשינוי של קבוצות של פריטים באמצעות כוונות מתוארים בקטעים הבאים.

גישה לאצווה

גישה באצווה לספק שימושית להוספה של מספר גדול של שורות, להוספת שורות במספר טבלאות באותה קריאה ל-method, ובאופן כללי לביצוע קבוצה של פעולות מעבר לגבולות התהליך כטרנזקציה, שנקראת פעולה אטומית.

כדי לגשת לספק במצב אצווה, צריך ליצור מערך של אובייקטים מסוג ContentProviderOperation ולשלוח אותם לספק תוכן באמצעות ContentResolver.applyBatch(). מעבירים למתודה הזו את הסמכות של ספק התוכן, ולא URI ספציפי של תוכן.

כך כל אובייקט ContentProviderOperation במערך יכול לפעול בטבלה אחרת. קריאה ל-ContentResolver.applyBatch() מחזירה מערך של תוצאות.

התיאור של סוג החוזה ContactsContract.RawContacts כולל קטע קוד שממחיש הוספה בכמות גדולה.

גישה לנתונים באמצעות כוונות

כוונות יכולות לספק גישה עקיפה לספק תוכן. אתם יכולים לאפשר למשתמש לגשת לנתונים של ספק גם אם לאפליקציה שלכם אין הרשאות גישה. לשם כך, אתם יכולים לקבל חזרה כוונה (intent) עם תוצאה מאפליקציה שיש לה הרשאות, או להפעיל אפליקציה שיש לה הרשאות ולאפשר למשתמש לבצע בה פעולות.

קבלת גישה עם הרשאות זמניות

אפשר לגשת לנתונים בספקי תוכן גם אם אין לכם את הרשאות הגישה המתאימות, על ידי שליחת כוונה (intent) לאפליקציה שיש לה את ההרשאות, וקבלת חזרה כוונה (intent) עם הרשאות URI. אלה הרשאות ל-URI ספציפי של תוכן, שתוקפן נמשך עד לסיום הפעילות שמקבלת אותן. האפליקציה עם ההרשאות הקבועות מעניקה הרשאות זמניות על ידי הגדרת דגל ב-Intent של התוצאה:

הערה: הדגלים האלה לא מעניקים גישה כללית לקריאה או לכתיבה לספק שהסמכות שלו כלולות ב-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, שמעניק לך גישה לכל אנשי הקשר של המשתמש ולכל המידע שלו, אפשר למשתמש להחליט באילו אנשי קשר האפליקציה תשתמש. כדי לעשות זאת, פועלים לפי התהליך הבא:

  1. באפליקציה, שולחים Intent שמכיל את הפעולה ACTION_PICK ואת סוג ה-MIME 'אנשי קשר' CONTENT_ITEM_TYPE, באמצעות השיטה startActivityForResult().
  2. מכיוון שהכוונה הזו תואמת למסנן הכוונה של הפעילות 'בחירה' באפליקציית 'אנשים', הפעילות הזו עוברת לחזית.
  3. בפעילות הבחירה, המשתמש בוחר איש קשר לעדכון. במקרים כאלה, פעילות הבחירה מפעילה setResult(resultcode, intent) כדי להגדיר כוונה להחזיר לאפליקציה שלך. ה-intent מכיל את ה-URI של התוכן של איש הקשר שבחר המשתמש ואת הדגלים FLAG_GRANT_READ_URI_PERMISSION של 'תוספים'. הדגלים האלה מעניקים לאפליקציה הרשאת URI לקרוא נתונים של איש הקשר שמפנים אליו באמצעות מזהה ה-URI של התוכן. לאחר מכן, פעילות הבחירה קוראת ל-finish() כדי להחזיר את השליטה לאפליקציה.
  4. הפעילות חוזרת לחזית והמערכת קוראת לשיטה onActivityResult() של הפעילות. השיטה הזו מקבלת את כוונת התוצאה שנוצרה על ידי פעילות הבחירה באפליקציית 'אנשים'.
  5. בעזרת ה-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 של תוכן.