Podstawowe informacje o dostawcy treści

Dostawca treści zarządza dostępem do centralnego repozytorium danych. Dostawca to część aplikacji na Androida, która często ma własny interfejs do pracy z danymi. Jednak dostawcy treści są przede wszystkim wykorzystywani przez inne aplikacje, które uzyskują dostęp do dostawcy za pomocą obiektu klienta dostawcy. Dostawcy i klienci dostawców oferują spójny, standardowy interfejs do danych, który obsługuje również komunikację międzyprocesową i bezpieczny dostęp do danych.

Zwykle współpracujesz z dostawcami treści w jednym z 2 scenariuszy: możesz wdrożyć kod, by uzyskać dostęp do istniejącego dostawcy treści w innej aplikacji, lub utworzyć w aplikacji nowego dostawcę treści, który będzie udostępniać dane innym aplikacjom.

Na tej stronie znajdziesz podstawowe informacje na temat współpracy z dotychczasowymi dostawcami treści. Więcej informacji o implementowaniu dostawców treści we własnych aplikacjach znajdziesz w artykule Tworzenie dostawcy treści.

Tematy w tym temacie:

  • Jak działają dostawcy treści.
  • Interfejs API używany do pobierania danych od dostawcy treści.
  • Interfejs API, którego używasz do wstawiania, aktualizowania lub usuwania danych u dostawcy treści.
  • Inne funkcje interfejsu API, które ułatwiają współpracę z dostawcami.

Przegląd

Dostawca treści przedstawia dane aplikacjom zewnętrznym w postaci co najmniej 1 tabeli podobnej do tabel w relacyjnej bazie danych. Wiersz reprezentuje wystąpienie jakiegoś typu danych gromadzonych przez dostawcę, a każda kolumna w tym wierszu reprezentuje pojedynczy fragment danych zebranych dla danej instancji.

Dostawca treści koordynuje dostęp do warstwy przechowywania danych w Twojej aplikacji dla wielu różnych interfejsów API i komponentów. Jak pokazano na ilustracji 1, są to następujące elementy:

  • Udostępnianie danych aplikacji innym aplikacjom
  • Wysyłanie danych do widżetu
  • Wyświetlanie niestandardowych sugestii wyszukiwania dotyczących aplikacji za pomocą platformy wyszukiwania z wykorzystaniem SearchRecentSuggestionsProvider
  • Synchronizowanie danych aplikacji z serwerem przy użyciu implementacji AbstractThreadedSyncAdapter
  • Wczytuję dane w interfejsie za pomocą CursorLoader
Związek między dostawcą treści a innymi komponentami.

Rysunek 1. Związek między dostawcą treści a innymi elementami.

Dostęp do dostawcy

Jeśli chcesz uzyskać dostęp do danych od dostawcy treści, musisz używać obiektu ContentResolver w Context aplikacji, aby komunikować się z tym dostawcą jako klientem. Obiekt ContentResolver komunikuje się z obiektem dostawcy, instancją klasy, która implementuje ContentProvider.

Obiekt dostawcy otrzymuje żądania danych od klientów, wykonuje żądane działanie i zwraca wyniki. Ten obiekt ma metody, które w obiekcie dostawcy wywołują metody o jednakowych nazwach – wystąpienie jednej z konkretnych podklas obiektu ContentProvider. Metody ContentResolver udostępniają podstawowe funkcje „CRUD” (tworzenia, pobierania, aktualizowania i usuwania) pamięci trwałej.

Typowe wzorce uzyskiwania dostępu do elementu ContentProvider z poziomu interfejsu użytkownika używają CursorLoader do uruchamiania asynchronicznego zapytania w tle. Activity lub Fragment w interfejsie wywołuje do zapytania metodę CursorLoader, która z kolei uzyskuje ContentProvider za pomocą metody ContentResolver.

Dzięki temu interfejs użytkownika będzie nadal dostępny, gdy zapytanie będzie działać. Ten wzorzec obejmuje interakcję szeregu różnych obiektów, a także bazowy mechanizm pamięci masowej, jak pokazano na rysunku 2.

Interakcja między obiektem ContentProvider, innymi klasami i miejscem na dane.

Rysunek 2. Interakcja między ContentProvider, innymi klasami i miejscem na dane.

Uwaga: aby uzyskać dostęp do dostawcy, aplikacja zwykle musi zażądać określonych uprawnień w swoim pliku manifestu. Ten wzorzec programowania został szczegółowo opisany w sekcji Uprawnienia dostawcy treści.

Jednym z dostawców wbudowanych na platformie Androida jest dostawca słownika użytkownika, który przechowuje niestandardowe słowa, które użytkownik chce zachować. Tabela 1 pokazuje, jak mogą wyglądać dane w tabeli tego dostawcy:

Tabela 1. Przykładowa tabela słownika użytkownika.

słowne identyfikator aplikacji publikowania region _IDENTYFIKATOR
mapreduce użytkownik1 100 pl_PL 1
precompiler użytkownik14 200 fr_FR 2
applet użytkownik2 225 fr_CA 3
const użytkownik1 255 pkt_BR 4
int użytkownik5 100 pl_PL 5

W tabeli 1 każdy wiersz reprezentuje wystąpienie słowa, którego nie ma w standardowym słowniku. Każda kolumna zawiera dane dotyczące tego słowa, np. język, w którym zostało ono po raz pierwszy napotkane. Nagłówki kolumn to nazwy kolumn przechowywane u dostawcy. Aby więc odwołać się do języka wiersza, np. użyj kolumny locale. W przypadku tego dostawcy kolumna _ID służy jako kolumna klucza podstawowego, którą dostawca automatycznie przechowuje.

Aby uzyskać listę słów i ich ustawień regionalnych od dostawcy słownika użytkownika, wywołaj ContentResolver.query(). Metoda query() wywołuje metodę ContentProvider.query() zdefiniowaną przez dostawcę słownika użytkownika. Te wiersze kodu zawierają wywołanie 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

Tabela 2 pokazuje, jak argumenty funkcji query(Uri,projection,selection,selectionArgs,sortOrder) pasują do instrukcji SQL SELECT:

Tabela 2: query() w porównaniu z zapytaniem SQL.

query() argument WYBIERZ słowo kluczowe/parametr Uwagi
Uri FROM table_name Mapa Uri jest mapowana na tabelę w dostawcy o nazwie table_name.
projection col,col,col,... projection to tablica kolumn dla każdego pobranego wiersza.
selection WHERE col = value selection określa kryteria wyboru wierszy.
selectionArgs Brak dokładnego odpowiednika. Argumenty wyboru zastępują obiekty zastępcze ? w klauzuli wyboru.
sortOrder ORDER BY col,col,... sortOrder określa kolejność, w jakiej wiersze pojawiają się w zwróconej wartości Cursor.

Identyfikatory URI treści

Identyfikator URI treści to identyfikator URI, który określa dane dostawcy. Identyfikatory URI treści obejmują symboliczną nazwę całego dostawcy (jej autoryzację) i nazwę wskazującą tabelę – ścieżkę. Gdy wywołujesz metodę klienta, aby uzyskać dostęp do tabeli u dostawcy, jednym z argumentów jest identyfikator URI treści tabeli.

W poprzednich wierszach kodu stała CONTENT_URI zawiera identyfikator URI treści tabeli Words dostawcy słownika użytkownika. Obiekt ContentResolver analizuje uprawnienia identyfikatora URI i używa go do określenia dostawcy przez porównanie uprawnień z tabelą systemową znanych dostawców. ContentResolver może wtedy wysyłać argumenty zapytania do właściwego dostawcy.

Aby wybrać tabelę, do której chcesz uzyskać dostęp, ContentProvider korzysta ze ścieżki identyfikatora URI treści. Dostawca zwykle ma ścieżkę dla każdej udostępnianej tabeli.

W poprzednich wierszach kodu pełny identyfikator URI tabeli Words to:

content://user_dictionary/words
  • Ciąg content:// to schemat, który jest zawsze obecny i identyfikuje go jako identyfikator URI treści.
  • Ciąg user_dictionary jest uprawnieniem dostawcy.
  • Ciąg words to ścieżka tabeli.

Wielu dostawców umożliwia dostęp do pojedynczego wiersza w tabeli przez dopisanie na końcu identyfikatora URI wartości identyfikatora. Aby np. pobrać wiersz, którego _ID to 4, z dostawcy słownika użytkownika, możesz użyć tego identyfikatora URI treści:

Kotlin

val singleUri: Uri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4)

Java

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

Wartości identyfikatorów często są używane, gdy po pobraniu zestawu wierszy chcesz zaktualizować lub usunąć jeden z nich.

Uwaga: klasy Uri i Uri.Builder zawierają wygodne metody tworzenia poprawnych obiektów URI na podstawie ciągów znaków. Klasa ContentUris zawiera wygodne metody dołączania wartości identyfikatorów do identyfikatora URI. Poprzedni fragment kodu korzysta z parametru withAppendedId(), aby dołączyć identyfikator do identyfikatora URI treści dostawcy słownika użytkownika.

Pobieranie danych od dostawcy

W tej sekcji opisujemy, jak pobrać dane od dostawcy, używając na przykład dostawcy słownika użytkownika.

Dla jasności – fragmenty kodu w tej sekcji wywołują element ContentResolver.query() w wątku interfejsu użytkownika. Tymczasem w prawdziwym kodzie zapytania można wykonywać asynchronicznie w osobnym wątku. Możesz użyć klasy CursorLoader, która została szczegółowo opisana w przewodniku Ładowarki. Co więcej, wiersze kodu to tylko fragmenty. Nie pokazują całego zgłoszenia.

Aby pobrać dane od dostawcy, wykonaj te podstawowe czynności:

  1. Poproś dostawcę o uprawnienia do odczytu.
  2. Zdefiniuj kod, który wysyła zapytanie do dostawcy.

Poproś o uprawnienia do odczytu

Aby pobrać dane od dostawcy, aplikacja musi mieć uprawnienia do odczytu dla tego dostawcy. Nie możesz prosić o te uprawnienia w czasie działania. Musisz wskazać, że potrzebujesz tych uprawnień w pliku manifestu. W tym celu użyj elementu <uses-permission> i dokładnej nazwy uprawnienia określonej przez dostawcę.

Określając ten element w pliku manifestu, żądasz tego uprawnienia dla swojej aplikacji. Użytkownicy instalują Twoją aplikację domyślnie, potwierdzając to żądanie.

Dokładną nazwę uprawnienia do odczytu używanego przez dostawcę, którego używasz, oraz nazwy innych uprawnień dostępu używanych przez dostawcę znajdziesz w jego dokumentacji.

Rola uprawnień w uzyskiwaniu dostępu do dostawców została szczegółowo opisana w sekcji Uprawnienia dostawców treści.

Dostawca słownika użytkownika definiuje w swoim pliku manifestu uprawnienie android.permission.READ_USER_DICTIONARY, dlatego aplikacja, która chce odczytywać dane od dostawcy, musi o nie prosić.

Tworzenie zapytania

Następnym krokiem pobierania danych od dostawcy jest utworzenie zapytania. Ten fragment kodu zawiera zmienne umożliwiające dostęp do dostawcy słownika użytkownika:

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 = {""};

W następnym fragmencie pokazujemy, jak używać funkcji ContentResolver.query(), na przykładzie dostawcy słownika użytkownika. Zapytanie klienta dostawcy jest podobne do zapytania SQL i zawiera zestaw kolumn do zwrócenia, zestaw kryteriów wyboru oraz kolejność sortowania.

Zbiór kolumn zwracanych przez zapytanie jest nazywany projekcją, a zmienna ma wartość mProjection.

Wyrażenie określające wiersze do pobrania jest podzielone na klauzulę wyboru i argumenty wyboru. Klauzula wyboru to kombinacja wyrażeń logicznych i logicznych, nazw kolumn i wartości. Zmienna to mSelectionClause. Jeśli zamiast wartości podasz parametr ?, metoda zapytania pobierze wartość z tablicy argumentów wyboru, którą jest zmienną mSelectionArgs.

Jeśli w następnym fragmencie użytkownik nie wpisze słowa, klauzula wyboru jest ustawiona na null, a zapytanie zwraca wszystkie słowa użyte przez dostawcę. Jeśli użytkownik wpisze słowo, klauzula wyboru ma wartość UserDictionary.Words.WORD + " = ?", a pierwszy element tablicy z argumentami wyboru jest ustawiany na słowo wpisane przez użytkownika.

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

}

To zapytanie jest podobne do tej instrukcji SQL:

SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

W tej instrukcji SQL zamiast stałych klas umowy używane są rzeczywiste nazwy kolumn.

Ochrona przed złośliwymi danymi

Jeśli dane zarządzane przez dostawcę treści znajdują się w bazie danych SQL, uwzględnienie zewnętrznych niezaufanych danych w nieprzetworzonych instrukcjach SQL może doprowadzić do wstrzykiwania SQL.

Rozważ taką klauzulę wyboru:

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;

W ten sposób użytkownik może połączyć złośliwy kod SQL z instrukcją SQL. Na przykład użytkownik może wpisać „nothing; DROP TABLE *;” w przypadku funkcji mUserInput, co spowoduje użycie klauzuli wyboru var = nothing; DROP TABLE *;.

Ponieważ klauzula wyboru jest traktowana jako instrukcja SQL, może to spowodować, że dostawca usunie wszystkie tabele w bazowej bazie danych SQLite, chyba że jest skonfigurowany do wyłapywania prób wstrzykiwania SQL.

Aby uniknąć tego problemu, użyj klauzuli wyboru, w której ? jako parametr wymienny, oraz oddzielną tablicę argumentów wyboru. Dzięki temu dane wejściowe użytkownika będą bezpośrednio powiązane z zapytaniem i nie będą interpretowane jako część instrukcji SQL. Nie jest traktowana jak kod SQL, więc dane wejściowe użytkownika nie mogą wstrzykiwać złośliwego kodu SQL. Zamiast konkatenacji do uwzględniania danych wejściowych użytkownika użyj tej klauzuli wyboru:

Kotlin

// Constructs a selection clause with a replaceable parameter
var selectionClause = "var = ?"

Java

// Constructs a selection clause with a replaceable parameter
String selectionClause =  "var = ?";

Ustaw tablicę argumentów wyboru w ten sposób:

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 = {""};

Umieść wartość w tablicy z argumentami wyboru w ten sposób:

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;

Preferowanym sposobem określania wyboru jest klauzula wyboru, w której używany jest ? jako parametr wymienny oraz tablica argumentów wyboru, nawet jeśli dostawca nie jest oparty na bazie danych SQL.

Wyświetl wyniki zapytania

Metoda klienta ContentResolver.query() zawsze zwraca Cursor zawierający kolumny określone przez prognozę zapytania dla wierszy spełniających kryteria wyboru zapytania. Obiekt Cursor zapewnia losowy dostęp z możliwością odczytu do wierszy i kolumn, które zawiera.

Korzystając z metod Cursor, możesz iterować wiersze w wynikach, określać typ danych w każdej kolumnie, pobierać dane z kolumny i analizować inne właściwości wyników.

Niektóre implementacje Cursor automatycznie aktualizują obiekt po zmianie danych dostawcy, aktywują metody w obiekcie obserwatora, gdy zmieni się Cursor, lub w obu przypadkach.

Uwaga: dostawca może ograniczyć dostęp do kolumn w zależności od charakteru obiektu generującego zapytanie. Dostawca kontaktów ogranicza na przykład dostęp do niektórych kolumn w celu synchronizacji adapterów, więc nie zwraca ich do aktywności lub usługi.

Jeśli żaden wiersz nie spełnia kryteriów wyboru, dostawca zwraca obiekt Cursor, dla którego Cursor.getCount() ma wartość 0, czyli pusty kursor.

Jeśli wystąpi błąd wewnętrzny, wyniki zapytania zależą od konkretnego dostawcy. Może zwrócić null lub zwrócić błąd Exception.

Element Cursor to lista wierszy, więc dobrym sposobem wyświetlenia zawartości obiektu Cursor jest połączenie go z obiektem ListView za pomocą SimpleCursorAdapter.

Poniższy fragment jest kontynuacją kodu z poprzedniego. Tworzy obiekt SimpleCursorAdapter zawierający obiekt Cursor pobrany przez zapytanie i ustawia ten obiekt jako adapter dla 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);

Uwaga: aby wyświetlić kopię zapasową ListView z Cursor, kursor musi zawierać kolumnę o nazwie _ID. Z tego powodu wyświetlone wcześniej zapytanie pobiera kolumnę _ID z tabeli Words, mimo że ListView jej nie wyświetla. To ograniczenie wyjaśnia też, dlaczego większość dostawców ma kolumnę _ID w przypadku każdej tabeli.

Pobierz dane z wyników zapytania

Poza wyświetlaniem wyników zapytań możesz ich używać do innych zadań. Możesz na przykład pobrać pisownię od dostawcy słownika użytkownika i wyszukać je u innych dostawców. Aby to zrobić, powtórzysz wiersze w tabeli Cursor, jak w tym przykładzie:

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
}

Implementacje interfejsu Cursor zawierają kilka metod „get” służących do pobierania różnych typów danych z obiektu. Na przykład w poprzednim fragmencie kodu jest używany element getString(). Mają też metodę getType(), która zwraca wartość wskazującą typ danych kolumny.

Uprawnienia dostawcy treści

Aplikacja dostawcy może określać uprawnienia, które inne aplikacje muszą mieć, aby uzyskać dostęp do danych dostawcy. Dzięki tym uprawnieniom użytkownik wie, do jakich danych aplikacja próbuje uzyskać dostęp. W zależności od wymagań dostawcy inne aplikacje żądają uprawnień dostępu do dostawcy. Użytkownicy widzą wymagane uprawnienia podczas instalowania aplikacji.

Jeśli aplikacja dostawcy nie określa żadnych uprawnień, inne aplikacje nie mają dostępu do danych dostawcy, chyba że zostanie on wyeksportowany. Dodatkowo komponenty w aplikacji dostawcy zawsze mają pełne uprawnienia do odczytu i zapisu, niezależnie od określonych uprawnień.

Dostawca słownika użytkownika wymaga uprawnienia android.permission.READ_USER_DICTIONARY, aby pobierać z niego dane. Dostawca ma osobne uprawnienie android.permission.WRITE_USER_DICTIONARY do wstawiania, aktualizowania i usuwania danych.

Aby uzyskać uprawnienia dostępu do dostawcy, aplikacja prosi o niego za pomocą elementu <uses-permission> w pliku manifestu. Gdy Menedżer pakietów na Androida zainstaluje aplikację, użytkownik musi zatwierdzić wszystkie uprawnienia, których żąda aplikacja. Jeśli użytkownik je zatwierdzi, Menedżer pakietów będzie kontynuować instalację. Jeśli użytkownik ich nie zatwierdzi, Menedżer pakietów zatrzyma instalację.

Ten przykładowy element <uses-permission> prosi o uprawnienia do odczytu dostawcy słownika użytkownika:

<uses-permission android:name="android.permission.READ_USER_DICTIONARY">

Wpływ uprawnień na dostęp dostawcy został szczegółowo wyjaśniony we wskazówkach dotyczących bezpieczeństwa.

Wstawianie, aktualizowanie i usuwanie danych

W taki sam sposób, w jaki pobierasz dane od dostawcy, do modyfikowania danych wykorzystujesz też interakcję między jego klientem a jego ContentProvider. Wywołujesz metodę ContentResolver z argumentami przekazywanymi do odpowiedniej metody ContentProvider. Dostawca i klient dostawcy automatycznie obsługują komunikację międzyprocesową i zabezpieczenia.

Wstaw dane

Aby wstawić dane do dostawcy, wywołaj metodę ContentResolver.insert(). Ta metoda wstawia nowy wiersz do dostawcy i zwraca identyfikator URI treści dla tego wiersza. Z tego fragmentu dowiesz się, jak wstawić nowe słowo do dostawcy słownika użytkownika:

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
);

Dane w nowym wierszu trafiają do pojedynczego obiektu ContentValues, który ma formę kursora jednowierszowego. Kolumny w tym obiekcie nie muszą mieć tego samego typu danych, a jeśli nie chcesz określać wartości, możesz ustawić kolumnę na null za pomocą funkcji ContentValues.putNull().

Poprzedni fragment kodu nie dodaje kolumny _ID, ponieważ jest ona utrzymywana automatycznie. Dostawca przypisuje unikalną wartość _ID do każdego dodawanego wiersza. Dostawcy zwykle używają tej wartości jako klucza podstawowego tabeli.

Identyfikator URI treści zwrócony w funkcji newUri identyfikuje nowo dodany wiersz w tym formacie:

content://user_dictionary/words/<id_value>

<id_value> to zawartość wiersza _ID w nowym wierszu. Większość dostawców może automatycznie wykryć identyfikator URI treści, a następnie wykonać żądaną operację na tym konkretnym wierszu.

Aby uzyskać wartość _ID ze zwróconego Uri, wywołaj ContentUris.parseId().

Zaktualizuj dane

Aby zaktualizować wiersz, używasz obiektu ContentValues ze zaktualizowanymi wartościami, tak jak w przypadku kryteriów wstawiania i wyboru, tak jak w przypadku zapytań. Używana przez Ciebie metoda klienta to ContentResolver.update(). Musisz tylko dodać wartości do obiektu ContentValues w przypadku kolumn, które aktualizujesz. Jeśli chcesz wyczyścić zawartość kolumny, ustaw wartość na null.

Poniższy fragment kodu zmienia wszystkie wiersze z językiem ustawionym "en" na null. Zwracana wartość to liczba zaktualizowanych wierszy.

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
);

Oczyść dane wejściowe użytkownika podczas wywoływania funkcji ContentResolver.update(). Więcej informacji na ten temat znajdziesz w sekcji Ochrona przed złośliwymi danymi.

Usuń dane

Usuwanie wierszy jest podobne do pobierania danych wiersza. Określasz kryteria wyboru wierszy, które chcesz usunąć, a metoda klienta zwraca liczbę usuniętych wierszy. Ten fragment kodu usuwa wiersze, których identyfikator aplikacji pasuje do "user". Metoda zwraca liczbę usuniętych wierszy.

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
);

Oczyść dane wejściowe użytkownika podczas wywoływania funkcji ContentResolver.delete(). Więcej informacji znajdziesz w sekcji Ochrona przed złośliwymi danymi.

Typy danych dostawców

Dostawcy treści mogą oferować wiele różnych typów danych. Dostawca słownika użytkownika oferuje tylko tekst, ale mogą też oferować te formaty:

  • Liczba całkowita
  • długa liczba całkowita (długie)
  • liczba zmiennoprzecinkowa
  • liczba zmiennoprzecinkowa długa (liczba zmiennoprzecinkowa)

Kolejnym typem danych często używanym przez dostawców jest duży obiekt binarny (BLOB) zaimplementowany jako tablica 64 KB. Dostępne typy danych możesz wyświetlić, korzystając z metod „get” klasy Cursor.

Typ danych w każdej kolumnie dostawcy jest zwykle podany w jego dokumentacji. Typy danych dostawcy słownika użytkownika są wymienione w dokumentacji referencyjnej jego klasy umowy (UserDictionary.Words). Klasy umowy zostały opisane w sekcji Klasy umowy. Możesz też określić typ danych, wywołując funkcję Cursor.getType().

Dostawcy przechowują też informacje o typie danych MIME dla każdego zdefiniowanego przez siebie identyfikatora URI treści. Dzięki informacjom o typie MIME możesz dowiedzieć się, czy Twoja aplikacja może obsługiwać dane oferowane przez dostawcę, lub wybrać sposób obsługi na podstawie typu MIME. Typ MIME zwykle jest potrzebny, gdy współpracujesz z dostawcą, który ma złożone struktury danych lub pliki.

Na przykład tabela ContactsContract.Data dostawcy kontaktów używa typów MIME do oznaczania typu danych kontaktów przechowywanych w poszczególnych wierszach. Aby uzyskać typ MIME odpowiadający identyfikatorowi URI treści, wywołaj ContentResolver.getType().

W sekcji Informacje o typach MIME opisaliśmy składnię standardowych i niestandardowych typów MIME.

Alternatywne formy dostępu do usługodawców

Podczas tworzenia aplikacji ważne są 3 alternatywne formy dostępu:

Dostęp wsadowy i modyfikowanie za pomocą intencji zostały opisane w kolejnych sekcjach.

Dostęp wsadowy

Dostęp wsadowy do dostawcy jest przydatny w przypadku wstawiania dużej liczby wierszy, wstawiania wierszy w wielu tabelach w tym samym wywołaniu metody oraz wykonywania zestawu operacji w granicach procesów jako transakcji, tzw. operacji niepodzielnych.

Aby uzyskać dostęp do dostawcy w trybie wsadowym, utwórz tablicę obiektów ContentProviderOperation, a następnie wyślij je do dostawcy treści za pomocą ContentResolver.applyBatch(). Zamiast konkretnego identyfikatora URI treści przekazujesz autorstwo dostawcy treści do tej metody.

Dzięki temu każdy obiekt ContentProviderOperation w tablicy może pracować z inną tabelą. Wywołanie ContentResolver.applyBatch() zwraca tablicę wyników.

Opis klasy umowy ContactsContract.RawContacts zawiera fragment kodu, który pokazuje wstawianie zbiorcze.

Dostęp do danych za pomocą intencji

Intencje mogą zapewniać pośredni dostęp do dostawcy treści. Możesz zezwolić użytkownikowi na dostęp do danych u dostawcy, nawet jeśli Twoja aplikacja nie ma uprawnień dostępu. Aby to zrobić, pobierz intencję wyniku z aplikacji, która ma uprawnienia, lub aktywuj aplikację, która ma uprawnienia, i pozwoli użytkownikowi na wykonywanie w niej operacji.

Uzyskiwanie dostępu dzięki tymczasowym uprawnieniom

Aby uzyskać dostęp do danych u dostawcy treści, nawet jeśli nie masz odpowiednich uprawnień dostępu, możesz wysłać intencję do aplikacji, która ma odpowiednie uprawnienia, i odebrać intencję wyniku z uprawnieniami URI. Są to uprawnienia dotyczące określonego identyfikatora URI treści, które obowiązują do momentu zakończenia działania, które je odbiera. Aplikacja, która ma stałe uprawnienia, przyznaje uprawnienia tymczasowe, ustawiając flagę w intencji wyników:

Uwaga: te flagi nie dają ogólnego dostępu do odczytu ani zapisu dostawcy, którego uprawnienia są zawarte w identyfikatorze URI treści. Dostęp dotyczy tylko identyfikatora URI.

Wysyłając identyfikatory URI treści do innej aplikacji, umieść co najmniej jedną z tych flag. Flagi udostępniają te funkcje każdej aplikacji, która otrzymuje intencję i jest kierowana na Androida 11 (poziom interfejsu API 30) lub nowszego:

  • Odczytuj dane, które reprezentuje identyfikator URI treści, lub do nich zapisuj, w zależności od flagi umieszczonej w intencji.
  • Zyskaj widoczność pakietu w aplikacji zawierającej dostawcę treści, który pasuje do identyfikatora URI. Aplikacja, która wysyła intencję, i aplikacja, która zawiera dostawcę treści, mogą to być 2 różne aplikacje.

Dostawca określa uprawnienia URI dla identyfikatorów URI treści w swoim manifeście przy użyciu atrybutu android:grantUriPermissions elementu <provider> oraz elementu podrzędnego <grant-uri-permission> elementu <provider>. Mechanizm uprawnień URI został szczegółowo wyjaśniony w przewodniku Uprawnienia na Androidzie.

Możesz na przykład pobrać dane kontaktu z dostawcy kontaktów, nawet jeśli nie masz uprawnienia READ_CONTACTS. Możesz to zrobić w aplikacji, która wysyła e-pozdrowienia do kontaktów w dniu urodzin. Zamiast wysyłać żądanie READ_CONTACTS, które daje Ci dostęp do wszystkich kontaktów użytkownika i ich informacji, pozwól mu kontrolować, których kontaktów używa Twoja aplikacja. W tym celu wykonaj następujące czynności:

  1. W aplikacji wyślij intencję zawierającą działanie ACTION_PICK i typ MIME „kontakty” CONTENT_ITEM_TYPE, korzystając z metody startActivityForResult().
  2. Ta intencja pasuje do filtra intencji dla działania „wybór” w aplikacji Osoby, więc aktywność działa na pierwszym planie.
  3. W działaniu wyboru użytkownik wybiera kontakt do zaktualizowania. W takim przypadku aktywność wyboru wywołuje funkcję setResult(resultcode, intent), aby skonfigurować intencję przekazania aplikacji. Intencja zawiera identyfikator URI treści kontaktu wybranego przez użytkownika oraz flagi „extras” FLAG_GRANT_READ_URI_PERMISSION. Te flagi przyznają aplikacji uprawnienia dotyczące identyfikatora URI do odczytu danych kontaktu wskazywanego przez identyfikator URI treści. W trakcie wybierania funkcja ta wywołuje metodę finish(), aby przywrócić kontrolę do aplikacji.
  4. Aktywność wraca do pierwszego planu, a system wywołuje jej metodę onActivityResult(). Ta metoda otrzymuje intencję wynikową utworzoną przez aktywność wyboru w aplikacji Osoby.
  5. Dzięki identyfikatorowi URI treści z intencji wynikowej możesz odczytywać dane kontaktu z dostawcy kontaktów, nawet jeśli w pliku manifestu nie prosisz dostawcy o trwałe uprawnienia do odczytu. Następnie możesz uzyskać informacje o dacie urodzenia lub adres e-mail tej osoby i wysłać e-maila z pozdrowieniem.

Użyj innej aplikacji

Innym sposobem umożliwienia użytkownikowi modyfikowania danych, do których nie masz uprawnień dostępu, jest aktywowanie aplikacji, która ma uprawnienia, i pozwolenie użytkownikowi na wykonywanie w niej tych czynności.

Na przykład aplikacja Kalendarz akceptuje intencję ACTION_INSERT, która umożliwia aktywowanie interfejsu wstawiania w aplikacji. W tej intencji możesz przekazywać dane „extras”, których aplikacja używa do wstępnego wypełniania interfejsu użytkownika. Wydarzenia cykliczne mają złożoną składnię, więc preferowanym sposobem wstawiania wydarzeń do dostawcy kalendarza jest aktywowanie aplikacji Kalendarz za pomocą elementu ACTION_INSERT i zezwolenie użytkownikowi na wstawienie wydarzenia w tym miejscu.

Wyświetlanie danych za pomocą aplikacji pomocniczej

Jeśli aplikacja ma uprawnienia dostępu, możesz nadal używać intencji wyświetlania danych w innej aplikacji. Na przykład aplikacja Kalendarz akceptuje intencję ACTION_VIEW, która wyświetla określoną datę lub wydarzenie. Dzięki temu możesz wyświetlać informacje z kalendarza bez konieczności tworzenia własnego interfejsu. Więcej informacji o tej funkcji znajdziesz w omówieniu dostawców kalendarza.

Aplikacja, do której wysyłasz intencję, nie musi być powiązana z dostawcą. Możesz na przykład pobrać kontakt z dostawcy kontaktu, a następnie wysłać do przeglądarki obrazów intencję ACTION_VIEW zawierającą identyfikator URI treści obrazu kontaktu.

Klasy umów

Klasa umowy definiuje stałe, które ułatwiają aplikacjom korzystanie z identyfikatorów URI treści, nazw kolumn, działań intencji i innych funkcji dostawcy treści. Klasy umowy nie są automatycznie uwzględniane u dostawcy. Deweloper dostawcy musi je zdefiniować, a następnie udostępnić innym deweloperom. Wielu dostawców dostępnych na platformie Android ma odpowiednie klasy umowy w pakiecie android.provider.

Na przykład Dostawca słownika użytkownika ma klasę umowy UserDictionary, która zawiera stałe identyfikatory URI treści i nazwy kolumny. Identyfikator URI treści tabeli Words jest zdefiniowany w stałej UserDictionary.Words.CONTENT_URI. Klasa UserDictionary.Words zawiera też stałe nazwy kolumn używane w przykładowych fragmentach kodu w tym przewodniku. Projekcja zapytania może być np. definiowana w następujący sposób:

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
};

Inna klasa umowy to ContactsContract dla dostawcy kontaktów. Dokumentacja referencyjna tej klasy zawiera przykładowe fragmenty kodu. Jedna z jej podklas, ContactsContract.Intents.Insert, jest klasą kontraktową, która zawiera stałe dla intencji i danych intencji.

Informacje o typach MIME

Dostawcy treści mogą zwracać standardowe typy multimediów MIME, niestandardowe ciągi typów MIME lub oba te rodzaje naraz.

Typy MIME mają następujący format:

type/subtype

Na przykład dobrze znany typ MIME text/html ma typ text i podtyp html. Jeśli dostawca zwraca ten typ dla identyfikatora URI, oznacza to, że zapytanie wykorzystujące ten identyfikator URI zwraca tekst zawierający tagi HTML.

Niestandardowe ciągi typów MIME, nazywane też typami MIME specyficznymi dla dostawców, mają bardziej złożone wartości type i subtype. W przypadku wielu wierszy wartość typu jest zawsze taka:

vnd.android.cursor.dir

W przypadku pojedynczego wiersza wartość typu jest zawsze taka:

vnd.android.cursor.item

Pole subtype jest specyficzne dla dostawcy. Wbudowani dostawcy Androida mają zwykle prosty podtyp. Gdy na przykład aplikacja Kontakty tworzy wiersz z numerem telefonu, ustawia w nim ten typ MIME:

vnd.android.cursor.item/phone_v2

Wartość podtypu to phone_v2.

Inni deweloperzy mogą tworzyć własne wzorce podtypów na podstawie uprawnień dostawcy i nazw tabel. Weźmy na przykład dostawcę, który udostępnia rozkłady jazdy pociągów. Uprawnienie dostawcy to com.example.trains i zawiera tabele Line1, Line2 i Line3. W odpowiedzi na następujący identyfikator URI treści dla wiersza 1 tabeli:

content://com.example.trains/Line1

dostawca zwraca następujący typ MIME:

vnd.android.cursor.dir/vnd.example.line1

W odpowiedzi na następujący identyfikator URI treści dla wiersza 5 w tabeli, wiersz 2:

content://com.example.trains/Line2/5

dostawca zwraca następujący typ MIME:

vnd.android.cursor.item/vnd.example.line2

Większość dostawców treści definiuje stałe klas umowy dla używanych typów MIME. Na przykład klasa umowy dostawcy kontaktów ContactsContract.RawContacts określa stałą CONTENT_ITEM_TYPE dla typu MIME pojedynczego nieprzetworzonego wiersza kontaktów.

Identyfikatory URI treści w pojedynczych wierszach są opisane w sekcji Identyfikatory URI treści.