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

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

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

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

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

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

סקירה כללית

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

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

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

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

גישה לספק

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

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

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

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

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

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

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

  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. לדוגמה, המשתמש יכול להזין 'שום דבר; 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() שמחזירה ערך שמציין סוג הנתונים של העמודה.

שחרור משאבים של תוצאות שאילתות

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 של הספק כדי לשנות נתונים. קוראים לשיטה של 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 רגילים וגם מותאמים אישית.

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

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

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

גישה בכמות גדולה

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

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

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

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

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

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

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

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

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

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