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. Dostawcy treści są jednak głównie używane przez inne aplikacje, które uzyskują dostęp do dostawcy za pomocą obiektu klienckiego dostawcy. Wspólnie dostawcy i klienci dostawców oferują spójny, standardowy interfejs danych, który obsługuje również komunikację między procesami i zapewnia bezpieczny dostęp do danych.

Zazwyczaj współpraca z dostawcami treści odbywa się w jednym z 2 sposobów: implementowanie kodu umożliwiającego dostęp do istniejącego dostawcy treści w innej aplikacji lub utworzenie nowego dostawcy treści w aplikacji, aby udostępniać dane innym aplikacjom.

Na tej stronie znajdziesz podstawowe informacje o pracy z istniejącymi dostawcami treści. Aby dowiedzieć się więcej o wdrażaniu dostawców treści w swoich aplikacjach, przeczytaj artykuł Tworzenie dostawcy treści.

W tym temacie omówiono te kwestie:

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

Omówienie

Dostawca treści udostępnia dane aplikacjom zewnętrznym w postaci co najmniej 1 tabeli podobnej do tabel w bazie danych relacyjnej. Wiersz reprezentuje instancję pewnego typu danych zbieranych przez dostawcę, a każda kolumna w wierszu reprezentuje pojedynczy element danych zebrany dla danej instancji.

Dostawca treści koordynuje dostęp do warstwy przechowywania danych w aplikacji dla wielu różnych interfejsów API i komponentów. Jak widać na rysunku 1, są to:

  • Przyznawanie dostępu do danych aplikacji innym aplikacjom
  • Wysyłanie danych do widżetu
  • Zwracanie niestandardowych sugestii wyszukiwania dla Twojej aplikacji za pomocą platformy wyszukiwania za pomocą funkcji SearchRecentSuggestionsProvider
  • Synchronizacja danych aplikacji z serwerem za pomocą implementacji AbstractThreadedSyncAdapter
  • Ładowanie danych w interfejsie użytkownika za pomocą CursorLoader
Relacja między dostawcą treści a innymi komponentami.

Rysunek 1. Relacja między dostawcą treści a innymi komponentami.

Dostęp do dostawcy

Aby uzyskać dostęp do danych u dostawcy treści, należy komunikować się z nim jako klient za pomocą obiektu ContentResolver w Context aplikacji. Obiekt ContentResolver komunikuje się z obiektem dostawcy, który jest instancją klasy implementującej interfejs ContentProvider.

Obiekt dostawcy otrzymuje żądania danych od klientów, wykonuje żądane działanie i zwraca wyniki. Ten obiekt ma metody, które wywołują metody o identycznej nazwie w obiekcie dostawcy, który jest wystąpieniem jednej z konkretnych podklas klasy ContentProvider. Metody ContentResolver udostępniają podstawowe funkcje CRUD (tworzenie, pobieranie, aktualizowanie i usuwanie) pamięci trwałej.

Typowy wzorzec dostępu do ContentProvider z interfejsu użytkownika korzysta z funkcji CursorLoader do wykonywania asynchronicznych zapytań w tle. Element Activity lub Fragment w interfejsie wywołuje funkcję CursorLoader, która zwraca dane ContentProvider za pomocą parametru ContentResolver.

Dzięki temu interfejs będzie nadal dostępny dla użytkownika, gdy zapytanie jest wykonywane. Ten wzorzec obejmuje interakcję kilku różnych obiektów, a także bazowego mechanizmu przechowywania danych, jak pokazano na ilustracji 2.

Interakcja między ContentProvider, innymi klasami i pamięcią.

Rysunek 2. Interakcja między usługą ContentProvider, innymi klasami i miejscem na dane.

Uwaga: aby uzyskać dostęp do dostawcy, aplikacja musi zwykle poprosić o określone uprawnienia w pliku manifestu. Ten schemat rozwoju został szczegółowo opisany w sekcji Uprawnienia dostawcy treści.

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

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

słowne identyfikator aplikacji publikowania region _ID
mapreduce użytkownik1 100 pl_PL 1
precompiler użytkownik14 200 fr_FR 2
applet użytkownik2 225 fr_CA 3
const user1 255 pt_BR 4
int user5 100 en_UK 5

W tabeli 1 każdy wiersz odpowiada wystąpieniu słowa, którego nie można znaleźć w standardowym słowniku. Każda kolumna zawiera część danych dotyczących danego słowa, np. język, w którym zostało ono po raz pierwszy użyte. Nagłówki kolumn to nazwy kolumn przechowywane u dostawcy. Aby na przykład odwołać się do regionu wiersza, możesz użyć kolumny locale. W przypadku tego dostawcy kolumna _ID pełni funkcję kolumny klucza podstawowego, którą dostawca automatycznie obsługuje.

Aby pobrać listę słów i ich języków z dostawcy słownika użytkownika, wywołaj ContentResolver.query(). Metoda query() wywołuje metodę ContentProvider.query() zdefiniowaną przez dostawcę słownika użytkownika. Poniższe wiersze kodu pokazują 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 zawiera informacje o tym, jak argumenty funkcji query(Uri,projection,selection,selectionArgs,sortOrder) pasują do instrukcji SQL SELECT:

Tabela 2. Porównanie zapytania query() z zapytaniem SQL.

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

Identyfikatory URI treści

Identyfikator URI treści to identyfikator URI identyfikujący dane w usługodawcy. Identyfikatory URI treści zawierają symboliczną nazwę całego dostawcy (jego autorytet) oraz nazwę wskazującą na tabelę (ścieżkę). Gdy wywołujesz metodę klienta, aby uzyskać dostęp do tabeli w dostawcy, identyfikator URI treści tej tabeli jest jednym z argumentów.

W poprzednich wierszach kodu stała CONTENT_URI zawiera identyfikator URI treści tabeli Words dostawcy słownika użytkownika. Obiekt ContentResolver analizuje autorytet identyfikatora URI i używa go do potwierdzenia dostawcy, porównując go z tabelą systemową znanych dostawców. ContentResolver może wtedy wysyłać argumenty zapytania do odpowiedniego dostawcy.

Funkcja ContentProvider używa części ścieżki w identyfikatorze URI treści, aby wybrać tabelę, do której ma uzyskać dostęp. Dostawca ma zwykle ścieżkę do każdej tabeli, którą udostępnia.

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 znaków user_dictionary to autorytet dostawcy.
  • Ciąg znaków words to ścieżka do tabeli.

Wielu dostawców umożliwia dostęp do pojedynczego wiersza w tabeli przez dodanie wartości identyfikatora do końca adresu URI. Aby na przykład pobrać wiersz, którego _ID to 4, z dostawcy słownika użytkownika, możesz użyć tego 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 są często używane, gdy pobierasz zbiór wierszy, a potem chcesz zaktualizować lub usunąć jeden z nich.

Uwaga: klasy UriUri.Builder zawierają wygodne metody do tworzenia poprawnie sformułowanych obiektów URI z ciągów znaków. Klasa ContentUris zawiera wygodne metody dołączania wartości identyfikatorów do identyfikatora URI. Poprzedni fragment kodu używa 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 jako przykładu dostawcy słownika użytkownika.

Dla ułatwienia fragmenty kodu w tej sekcji wywołują funkcję ContentResolver.query() w wątku interfejsu. W samym kodzie jednak wykonuj zapytania asynchronicznie na osobnym wątku. Możesz użyć klasy CursorLoader, która jest opisana bardziej szczegółowo w przewodniku Loaders. Ponadto wiersze kodu to tylko fragmenty. Nie pokazują one pełnej aplikacji.

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

  1. Poproś o uprawnienia do odczytu dla dostawcy.
  2. Określ kod, który wysyła zapytanie do dostawcy.

Poproś o dostęp do odczytu

Aby pobrać dane od dostawcy, aplikacja musi mieć uprawnienia dostępu do odczytu. Nie możesz poprosić o to uprawnienie w czasie działania aplikacji. Zamiast tego musisz określić w pliku manifestu, że potrzebujesz tego uprawnienia, używając elementu <uses-permission> i dokładnej nazwy uprawnienia zdefiniowanej przez dostawcę.

Gdy określisz ten element w pliku manifestu, żądasz tych uprawnień dla swojej aplikacji. Gdy użytkownicy instalują aplikację, domyślnie wyrażają zgodę na to żądanie.

Dokładną nazwę uprawnienia dostępu do odczytu dla używanego dostawcy oraz nazwy innych uprawnień dostępu używanych przez dostawcę znajdziesz w jego dokumentacji.

Rola uprawnień w dostępie do dostawców jest bardziej szczegółowo opisana w sekcji Uprawnienia dostawcy treści.

Dostawca słownika użytkownika definiuje uprawnienie android.permission.READ_USER_DICTIONARY w pliku manifestu, więc aplikacja, która chce odczytywać dane od dostawcy, musi poprosić o to uprawnienie.

Tworzenie zapytania

Kolejnym krokiem w pobieraniu danych od dostawcy jest sformułowanie zapytania. Ten fragment kodu definiuje niektóre 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 = {""};

Następny fragment kodu pokazuje, jak korzystać z pola ContentResolver.query(), na przykładzie dostawcy słownika użytkownika. Zapytanie klienta dostawcy jest podobne do zapytania SQL i zawiera zbiór kolumn do zwrócenia, zbiór kryteriów wyboru oraz kolejność sortowania.

Zbiór kolumn zwracanych przez zapytanie jest nazywany odwzorowaniem, 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 algorytmicznych, nazw kolumn oraz wartości. Zmienna to mSelectionClause. Jeśli zamiast wartości podasz parametr wymienny ?, metoda zapytania pobiera wartość z tablicy argumentów wyboru, która jest zmienną mSelectionArgs.

Jeśli w następnym fragmencie kodu użytkownik nie wpisze słowa, klauzula wyboru zostanie ustawiona na null , a zapytanie zwróci wszystkie słowa w dostawcy. Jeśli użytkownik wpisze słowo, klauzula wyboru zostanie ustawiona na UserDictionary.Words.WORD + " = ?", a pierwszy element tablicy argumentów wyboru zostanie ustawiony na słowo wprowadzone 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 analogiczne do tego wyrażenia SQL:

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

W tym poleceniu SQL zamiast stałych wartości klasy umowy używane są rzeczywiste nazwy kolumn.

Ochrona przed złośliwymi danymi wejściowymi

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

Rozważ następującą 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;

Jeśli to zrobisz, użytkownik będzie mógł dołączyć do instrukcji SQL potencjalnie szkodliwy kod. Użytkownik może na przykład wpisać „nothing; DROP TABLE *;” w przypadku mUserInput, co spowoduje utworzenie klauzuli wyboru var = nothing; DROP TABLE *;.

Ponieważ klauzula wyboru jest traktowana jak instrukcja SQL, może to spowodować wymazanie przez dostawcę wszystkich tabel w bazie danych SQLite, chyba że dostawca jest skonfigurowany tak, aby przechwytywać próby wstrzyknięcia kodu SQL.

Aby uniknąć tego problemu, użyj klauzuli wyboru, w której parametr ? jest wymiennym parametrem, oraz osobnej tablicy argumentów wyboru. Dzięki temu dane wejściowe użytkownika są powiązane bezpośrednio z zapytaniem, a nie interpretowane jako część instrukcji SQL. Dane wejściowe użytkownika nie są traktowane jako kod SQL, więc nie mogą zawierać złośliwego kodu SQL. Zamiast stosowania koniunkcji 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 = ?";

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

W tablicy argumentów selection podaj wartość 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;

Klauzula wyboru, która używa ? jako parametru wymiennego i tablicy argumentów wyboru, jest preferowanym sposobem określania wyboru, nawet jeśli dostawca nie korzysta z bazy danych SQL.

Wyświetlanie wyników zapytania

Metoda klienta ContentResolver.query() zawsze zwraca Cursor zawierający kolumny określone przez projekcję zapytania w przypadku wierszy, które pasują do kryteriów wyboru zapytania. Obiekt Cursor zapewnia losowy dostęp do odczytu wierszy i kolumn, które zawiera.

Metody Cursor umożliwiają iterację wierszy w wynikach, określanie typu danych w każdej kolumnie, pobieranie danych z kolumny i sprawdzanie innych właściwości wyników.

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

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

Jeśli żadne wiersze nie pasują do kryteriów wyboru, dostawca zwraca obiekt Cursor, dla którego Cursor.getCount() jest równe 0, czyli pusty kursor.

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

Ponieważ element Cursor to lista wierszy, dobrym sposobem na wyświetlenie zawartości elementu Cursor jest połączenie go z elementem ListView za pomocą elementu SimpleCursorAdapter.

Poniższy fragment stanowi kontynuację kodu z poprzedniego. Tworzy obiekt SimpleCursorAdapter zawierający obiekt Cursor, który został pobrany przez zapytanie, i ustala, że ten obiekt ma być adapterem dla obiektu 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 zastąpić ListView wartością Cursor, kursor musi zawierać kolumnę o nazwie _ID. Z tego powodu zapytanie pokazane wcześniej zwraca kolumnę _ID tabeli Words, mimo że tabela ListView jej nie zawiera. To ograniczenie wyjaśnia również, dlaczego większość dostawców ma kolumnę _ID w przypadku każdej swojej tabeli.

Pobieranie danych z wyników zapytania

Oprócz wyświetlania wyników zapytań możesz ich używać do innych zadań. Możesz na przykład pobrać pisownię od dostawcy słownika użytkownika, a następnie wyszukać je u innych dostawców. Aby to zrobić, powtarzaj kolejne wiersze w tabeli Cursor, jak pokazano 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 Cursor zawierają kilka metod „get” służących do pobierania różnych typów danych z obiektu. Poprzedni fragment kodu używa na przykład funkcji getString(). Mają też metodę getType(), która zwraca wartość wskazującą typ danych kolumny.

Zwolnij zasoby wyników zapytania

Obiekty Cursor muszą zostać zamknięte, jeśli nie są już potrzebne, aby powiązane z nimi zasoby zostały zwolnione wcześniej. Można to zrobić, wywołując funkcję close() lub używając instrukcji try-with-resources w języku programowania Java albo funkcji use() w języku programowania Kotlin.

Uprawnienia dostawcy treści

Aplikacja dostawcy może określać uprawnienia, które inne aplikacje muszą mieć, aby uzyskać dostęp do danych dostawcy. Te uprawnienia informują użytkownika, do jakich danych aplikacja próbuje uzyskać dostęp. Zgodnie z wymaganiami dostawcy inne aplikacje wymagają uprawnień potrzebnych do uzyskania dostępu do dostawcy. Użytkownicy widzą żądane 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 mają zawsze pełny dostęp 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 oddzielne uprawnienia android.permission.WRITE_USER_DICTIONARY do wstawiania, aktualizowania i usuwania danych.

Aby uzyskać uprawnienia dostępu do dostawcy, aplikacja żąda ich za pomocą elementu <uses-permission> w pliku manifestu. Gdy Menedżer pakietów Androida instaluje aplikację, użytkownik musi zaakceptować wszystkie uprawnienia, o które prosi aplikacja. Jeśli użytkownik je zaakceptuje, menedżer pakietów kontynuuje instalację. Jeśli użytkownik ich nie zatwierdzi, Menedżer pakietów przerywa instalację.

Ten przykładowy element <uses-permission> prosi o dostęp do odczytu w usłudze Dostawca 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

Podobnie jak w przypadku pobierania danych od dostawcy, do modyfikowania danych możesz też używać interakcji między klientem dostawcy a jego usługą ContentProvider. Wywołujesz metodę ContentResolver z argumentami przekazywanymi do odpowiedniej metody ContentProvider. Dostawca i klient dostawcy automatycznie obsługują bezpieczeństwo i komunikację między procesami.

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 tego wiersza. Ten fragment kodu pokazuje, jak w dostawcy słownika użytkownika wstawić nowe słowo:

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 nowego wiersza trafiają do pojedynczego obiektu ContentValues, który ma podobną formę do kursora jednowierszowego. Kolumny w tym obiekcie nie muszą zawierać tego samego typu danych. Jeśli w ogóle 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ż ta kolumna jest aktualizowana automatycznie. Dostawca przypisuje niepowtarzalną wartość _ID do każdego dodanego wiersza. Dostawcy zwykle używają tej wartości jako klucza podstawowego tabeli.

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

content://user_dictionary/words/<id_value>

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

Aby uzyskać wartość _ID z zwróconej wartości Uri, wywołaj funkcję ContentUris.parseId().

Aktualizowanie danych

Aby zaktualizować wiersz, użyj obiektu ContentValues z zaktualizowanymi wartościami, tak jak w przypadku wstawienia, oraz kryteriów wyboru, tak jak w przypadku zapytania. Używana przez Ciebie metoda klienta to ContentResolver.update(). Do obiektu ContentValues musisz dodać tylko wartości w przypadku kolumn, które aktualizujesz. Jeśli chcesz wyczyścić zawartość kolumny, ustaw wartość na null.

Ten fragment kodu zmienia wszystkie wiersze, których ustawienia regionalne mają język "en", na takie, które mają ustawienia regionalne 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
);

Przefiltruj dane wejściowe użytkownika podczas rozmowy.ContentResolver.update() Więcej informacji na ten temat znajdziesz w sekcji Ochrona przed złośliwymi danymi wejściowymi.

Usuń dane

Usuwanie wierszy jest podobne do pobierania danych wierszy. 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ę skasowanych 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
);

Przetwarzaj dane wejściowe użytkownika podczas rozmowy telefonicznej.ContentResolver.delete() Więcej informacji na ten temat znajdziesz w sekcji Ochrona przed złośliwymi danymi wejściowymi.

Typy danych dostawcy

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

  • Liczba całkowita
  • długa liczba całkowita (długi)
  • zmiennoprzecinkowa
  • długie zmiennoprzecinkowe (podwójne)

Innym typem danych, którego dostawcy często używają, jest duży obiekt binarny (BLOB) implementowany jako tablica o rozmiary 64 KB. Aby zobaczyć dostępne typy danych, spójrz na metody „get” klasy Cursor.

Typ danych dla każdej kolumny dostawcy jest zwykle wymieniony w jego dokumentacji. Typy danych dostawcy słownika użytkownika są wymienione w dokumentacji referencyjnej dotyczącej klasy umowy UserDictionary.Words. Klasy kontraktów są opisane w sekcji Klasy kontraktów. Typ danych możesz też określić, wywołując funkcję Cursor.getType().

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

Na przykład tabela ContactsContract.Data w usługodawcy kontaktów używa typów MIME do oznaczania typu danych kontaktowych przechowywanych w każdej kolumnie. Aby uzyskać typ MIME odpowiadający identyfikatorowi URI treści, wywołaj ContentResolver.getType().

W sekcji Przewodnik po typach MIME opisano składnię typów MIME standardowych i niestandardowych.

Alternatywy dostępu dostawcy

W procesie tworzenia aplikacji ważne są 3 alternatywne formy dostępu do usług:

W kolejnych sekcjach opisano dostęp do zbiorczego przetwarzania i modyfikowania za pomocą intencji.

Dostęp zbiorczy

Dostęp zbiorczy do dostawcy jest przydatny do wstawiania dużej liczby wierszy, wstawiania wierszy w wielu tabelach w tym samym wywołaniu metody oraz ogólnie do wykonywania zestawu operacji na granicach procesów jako transakcji, zwanej operacją atomową.

Aby uzyskać dostęp do dostawcy w trybie zbiorczym, utwórz tablicę obiektów ContentProviderOperation, a następnie prześlij je do dostawcy treści za pomocą funkcji ContentResolver.applyBatch(). Do tej metody przekazujesz autorytet dostawcy treści, a nie konkretny identyfikator URI treści.

Dzięki temu każdy obiekt ContentProviderOperation w tablicy działa na podstawie innej tabeli. Wywołanie ContentResolver.applyBatch() zwraca tablicę wyników.

Opis klasy umowy ContactsContract.RawContacts zawiera fragment kodu przedstawiający wsadowe wstawianie.

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 w usługodawcy, nawet jeśli Twoja aplikacja nie ma uprawnień dostępu. Możesz to zrobić, uzyskując intencję wyniku z aplikacji, która ma uprawnienia, lub aktywując aplikację, która ma uprawnienia, i zezwalanie użytkownikowi na pracę w niej.

Uzyskiwanie dostępu za pomocą uprawnień tymczasowych

Możesz uzyskać dostęp do danych dostawcy treści, nawet jeśli nie masz odpowiednich uprawnień dostępu, wysyłając intencję do aplikacji, która ma te uprawnienia, i otrzymywać z powrotem intencję wyniku zawierającą uprawnienia URI. To uprawnienia do określonego identyfikatora URI treści, które są ważne do czasu zakończenia aktywności, w ramach której zostały przyznane. Aplikacja, która ma stałe uprawnienia, przyznaje uprawnienia tymczasowe, ustawiając flagę w intencji wyniku:

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

Podczas wysyłania identyfikatorów URI treści do innej aplikacji dołącz co najmniej 1 z tych flag. Flagi zapewniają następujące możliwości każdej aplikacji, która otrzymuje intencję i jest kierowana na Androida 11 (poziom interfejsu API 30) lub nowszego:

  • odczytywać lub zapisywać dane reprezentowane przez identyfikator URI treści, w zależności od flagi uwzględnionej w intencji;
  • Uzyskaj widoczność pakietu w aplikacji zawierającej dostawcę treści pasującego do urzędu certyfikacji URI. Aplikacja wysyłająca intencję i aplikacja zawierająca dostawcę treści mogą być 2 różnymi aplikacjami.

Dostawca definiuje uprawnienia URI dla identyfikatorów URI treści w swoim pliku manifestu, używając atrybutu android:grantUriPermissions elementu <provider> oraz elementu podrzędnego <grant-uri-permission> elementu <provider>. Mechanizm uprawnień URI jest szczegółowo omówiony w przewodniku Uprawnienia na Androidzie.

Możesz na przykład pobrać dane kontaktu u dostawcy kontaktów, nawet jeśli nie masz uprawnienia READ_CONTACTS. Możesz to zrobić w aplikacji, która wysyła e-maile z życzeniami urodzinowymi do kontaktów w ich urodziny. Zamiast prosić o READ_CONTACTS, co daje dostęp do wszystkich kontaktów i informacji użytkownika, pozwól użytkownikowi decydować, których kontaktów ma używać Twoja aplikacja. Aby to zrobić:

  1. W swojej aplikacji wyślij intencję zawierającą działanie ACTION_PICK i typ MIME CONTENT_ITEM_TYPE „kontaktów”, korzystając z metody startActivityForResult().
  2. Ta intencja pasuje do filtra intencji dla aktywności „wyboru” w aplikacji Kontakty, więc ta aktywność staje się aktywna.
  3. W aktywności zaznaczania użytkownik wybiera kontakt do zaktualizowania. W takim przypadku działanie wyboru wywołuje setResult(resultcode, intent), aby skonfigurować intencję odwrócenia aplikacji. Intencja zawiera identyfikator URI treści kontaktu wybranego przez użytkownika i flagi „dodatkowe”FLAG_GRANT_READ_URI_PERMISSION. Te flagi przyznają aplikacji uprawnienia identyfikatora URI do odczytu danych o kontakcie wskazywanym przez identyfikator URI treści. Następnie aktywność wyboru wywołuje funkcję finish(), aby przekazać kontrolę aplikacji.
  4. Aktywność wraca na pierwszy plan, a system wywołuje metodę onActivityResult() tej aktywności. Ta metoda otrzymuje zamiar wyniku utworzony przez aktywność wyboru w aplikacji Kontakty.
  5. Dzięki URI treści z intencji wyniku możesz odczytać dane kontaktu z dostawcy kontaktów, nawet jeśli w manifeście nie poprosisz o trwałe uprawnienia dostępu do odczytu dla tego dostawcy. Możesz wtedy uzyskać informacje o urodzinach kontaktu lub adres e-mail, a następnie wysłać e-maila z życzeniami.

Używanie innej aplikacji

Innym sposobem na umożliwienie użytkownikowi modyfikowania danych, do których nie masz uprawnień dostępu, jest aktywowanie aplikacji, która ma takie uprawnienia, i pozostawienie pracy użytkownikowi.

Na przykład aplikacja Kalendarz akceptuje intencję ACTION_INSERT, która umożliwia aktywowanie interfejsu wstawiania aplikacji. W ramach tego zamiaru możesz przekazać „dodatkowe” dane, których aplikacja użyje do wstępnego wypełnienia interfejsu. Wydarzenia cykliczne mają złożoną składnię, dlatego preferowanym sposobem wstawiania wydarzeń do dostawcy kalendarza jest aktywowanie aplikacji Kalendarz za pomocą identyfikatora ACTION_INSERT, a następnie zezwolenie użytkownikowi na wstawienie w nim wydarzenia.

Wyświetlanie danych za pomocą aplikacji pomocniczej

Jeśli Twoja aplikacja ma uprawnienia dostępu, możesz nadal używać komend, aby wyświetlać dane w innej aplikacji. Na przykład aplikacja Kalendarz akceptuje intencję ACTION_VIEW, która wyświetla konkretną datę lub wydarzenie. Dzięki temu możesz wyświetlać informacje z kalendarza bez konieczności tworzenia własnego interfejsu użytkownika. Więcej informacji o tej funkcji znajdziesz w artykule Omówienie funkcji kalendarza.

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

Klasy kontraktowe

Klasa kontraktu definiuje stałe, które pomagają aplikacjom w obsługiwaniu URI treści, nazw kolumn, działań związanych z zamiarem i innych funkcji dostawcy treści. Zajęcia kontraktowe nie są automatycznie uwzględniane w przypadku dostawcy. Programista dostawcy musi je zdefiniować, a następnie udostępnić innym programistom. Wielu dostawców usług dostępnych na platformie Androida ma odpowiednie klasy umowy w pakiecie android.provider.

Na przykład dostawca słownika użytkownika ma klasę kontraktu UserDictionary zawierającą stałe identyfikatorów URI treści i nazwy kolumn. Identyfikator URI treści w tabeli Words jest zdefiniowany w stałym parametrze UserDictionary.Words.CONTENT_URI. Klasa UserDictionary.Words zawiera też stałe nazw kolumn, które są używane w przykładowych fragmentach kodu w tym przewodniku. Projekcję zapytania można zdefiniować na przykład w ten 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
};

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

Odniesienie do typu MIME

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

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 w przypadku identyfikatora URI, oznacza to, że zapytanie z użyciem tego identyfikatora zwraca tekst zawierający tagi HTML.

Niestandardowe ciągi znaków typu MIME, zwane też typami MIME zależnymi od dostawcy, 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

Funkcja subtype jest specyficzna dla dostawcy. Wbudowani dostawcy Androida mają zwykle prosty podtyp. Na przykład gdy aplikacja Kontakty tworzy wiersz dla numeru telefonu, ustawia w nim następujący typ MIME:

vnd.android.cursor.item/phone_v2

Wartością podtypu jest phone_v2.

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

content://com.example.trains/Line1

dostawca zwraca ten typ MIME:

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

W odpowiedzi na ten identyfikator URI treści dla wiersza 5 w tabeli Line2:

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

dostawca zwraca ten typ MIME:

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

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

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