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
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.
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:
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:
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 Uri
i Uri.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:
- Poproś o uprawnienia do odczytu dla dostawcy.
- 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:
-
Dostęp zbiorczy: możesz utworzyć zbiorczy zestaw wywołań dostępu za pomocą metod w klasie
ContentProviderOperation
, a potem zastosować je za pomocą funkcjiContentResolver.applyBatch()
. -
Zapytania asynchroniczne: zapytania w osobnym wątku. Możesz użyć obiektu
CursorLoader
. Przykłady w przewodniku dotyczącym modułów wczytujących pokazują, jak to zrobić. - Dostęp do danych za pomocą intencji: chociaż nie można wysłać intencji bezpośrednio do dostawcy, można wysłać do niego intencję, która zwykle jest najlepszym wyposażeniem do modyfikowania danych dostawcy.
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:
-
Uprawnienie do odczytu:
FLAG_GRANT_READ_URI_PERMISSION
-
Uprawnienia do zapisu:
FLAG_GRANT_WRITE_URI_PERMISSION
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ć:
-
W swojej aplikacji wyślij intencję zawierającą działanie
ACTION_PICK
i typ MIMECONTENT_ITEM_TYPE
„kontaktów”, korzystając z metodystartActivityForResult()
. - Ta intencja pasuje do filtra intencji dla aktywności „wyboru” w aplikacji Kontakty, więc ta aktywność staje się aktywna.
-
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. -
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. - 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.