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
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.
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:
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:
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:
- Poproś dostawcę o uprawnienia do odczytu.
- 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: możesz utworzyć grupę wywołań dostępu za pomocą metod w klasie
ContentProviderOperation
, a następnie zastosować je za pomocąContentResolver.applyBatch()
. -
Zapytania asynchroniczne: wysyłaj je w osobnym wątku. Możesz użyć obiektu
CursorLoader
. Przykłady w przewodniku Loaders (Moduły wczytywania) 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ć intencję do aplikacji dostawcy, która zwykle jest najlepiej wyposażona do modyfikowania danych dostawcy.
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:
-
Uprawnienia do odczytu:
FLAG_GRANT_READ_URI_PERMISSION
-
Uprawnienia do zapisu:
FLAG_GRANT_WRITE_URI_PERMISSION
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:
-
W aplikacji wyślij intencję zawierającą działanie
ACTION_PICK
i typ MIME „kontakty”CONTENT_ITEM_TYPE
, korzystając z metodystartActivityForResult()
. - Ta intencja pasuje do filtra intencji dla działania „wybór” w aplikacji Osoby, więc aktywność działa na pierwszym planie.
-
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. -
Aktywność wraca do pierwszego planu, a system wywołuje jej metodę
onActivityResult()
. Ta metoda otrzymuje intencję wynikową utworzoną przez aktywność wyboru w aplikacji Osoby. - 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.