Utwórz dostawcę treści

Dostawca treści zarządza dostępem do centralnego repozytorium danych. Dostawca implementujesz jako co najmniej 1 klasę w aplikacji na Androida wraz z elementami w pliku manifestu. Jedna z klas implementuje podklasę ContentProvider, która jest interfejsem między dostawcą a innymi aplikacjami.

Chociaż dostawcy treści są przeznaczone do udostępniania danych innym aplikacjom, aplikacja może mieć działania, które umożliwiają użytkownikom wysyłanie zapytań i modyfikowanie danych zarządzanych przez dostawcę.

Ta strona zawiera opis podstawowego procesu tworzenia dostawcy treści oraz listę interfejsów API, których należy użyć.

Zanim zaczniesz tworzyć

Zanim zaczniesz tworzyć dostawcę, weź pod uwagę te kwestie:

  • Zdecyduj, czy potrzebujesz dostawcy treści. Musisz utworzyć dostawcę treści, jeśli chcesz udostępnić co najmniej jedną z tych funkcji:
    • chcesz zaoferować złożone dane lub pliki innym aplikacjom;
    • Chcesz umożliwić użytkownikom kopiowanie złożonych danych z Twojej aplikacji do innych aplikacji.
    • Chcesz podać niestandardowe sugestie wyszukiwania za pomocą platformy wyszukiwania.
    • Chcesz udostępnić dane aplikacji widżetom.
    • Chcesz wdrożyć klasy AbstractThreadedSyncAdapter, CursorAdapter lub CursorLoader.

    Nie musisz używać dostawcy baz danych ani innych typów trwałej pamięci masowej, jeśli taka pamięć jest używana w całości w Twojej aplikacji i nie potrzebujesz żadnej z wymienionych wcześniej funkcji. Zamiast tego możesz użyć jednego z systemów pamięci opisanych w artykule Przechowywanie danych i plików.

  • Przeczytaj podstawowe informacje o dostawcach treści, aby dowiedzieć się więcej o dostawcach i ich działaniu.

Następnie wykonaj te czynności, aby utworzyć dostawcę:

  1. Projektowanie nieprzetworzonej pamięci masowej na dane. Dostawca treści udostępnia dane na 2 sposoby:
    Dane pliku
    Dane, które zwykle trafiają do plików, takie jak zdjęcia, pliki audio czy filmy. Przechowuj pliki w przestrzeni prywatnej aplikacji. W odpowiedzi na żądanie udostępnienia pliku z innej aplikacji dostawca może zaoferować uchwyt pliku.
    „Uporządkowane” dane
    Dane, które zwykle trafiają do bazy danych, tablicy lub podobnej struktury. Przechowuj dane w formacie zgodnym z tabelami wierszy i kolumn. Wiersz reprezentuje element, np. osobę lub element w asortymencie. Kolumna zawiera niektóre dane dotyczące elementu, takie jak imię i nazwisko osoby lub cena produktu. Typowym sposobem przechowywania tego typu danych jest baza danych SQLite, ale możesz korzystać z dowolnego typu pamięci trwałej. Więcej informacji o typach pamięci dostępnych w systemie Android znajdziesz w sekcji Przechowywanie danych projektu.
  2. Zdefiniuj konkretną implementację klasy ContentProvider i jej wymaganych metod. Ta klasa łączy Twoje dane z resztą systemu Android. Więcej informacji o tej klasie znajdziesz w sekcji Implementowanie klasy ContentProvider.
  3. Zdefiniuj ciąg urzędowy dostawcy, identyfikatory URI treści i nazwy kolumn. Jeśli chcesz, aby aplikacja dostawcy obsługiwała intencje, zdefiniuj też działania intencji, dodatkowe dane i flagi. Określ też uprawnienia wymagane dla aplikacji, które chcą mieć dostęp do Twoich danych. Rozważ zdefiniowanie wszystkich tych wartości jako stałych w osobnej klasie umowy. Później możesz udostępnić tę klasę innym programistom. Więcej informacji o identyfikatorach URI treści znajdziesz w sekcji Identyfikatory URI treści projektu. Więcej informacji o intencjach znajdziesz w sekcji Intencje i dostęp do danych.
  4. Dodaj inne opcjonalne elementy, takie jak przykładowe dane lub implementacja AbstractThreadedSyncAdapter, która może synchronizować dane między dostawcą a danymi w chmurze.

Przechowywanie danych projektu

Dostawca treści to interfejs do danych zapisanych w uporządkowanym formacie. Przed utworzeniem interfejsu zdecyduj, jak chcesz przechowywać dane. Możesz przechowywać dane w dowolnej postaci, a później zaprojektować interfejs tak, aby w razie potrzeby odczytywać i zapisywać dane.

Oto niektóre technologie przechowywania danych dostępne na Androidzie:

  • Jeśli używasz uporządkowanych danych, rozważ relacyjną bazę danych, np. SQLite, lub nierelacyjny magazyn danych klucz-wartość, np. LevelDB. Jeśli używasz nieuporządkowanych danych, np. plików audio, graficznych lub wideo, rozważ przechowywanie ich w postaci plików. Możesz łączyć i dopasowywać kilka różnych typów pamięci masowej i w razie potrzeby udostępniać je za pomocą jednego dostawcy treści.
  • System Android może korzystać z biblioteki trwałości sal, która zapewnia dostęp do interfejsu API bazy danych SQLite, którego dostawcy Androida używają do przechowywania danych tabelarycznych. Aby utworzyć bazę danych za pomocą tej biblioteki, utwórz instancję podklasy RoomDatabase zgodnie z opisem w sekcji Zapisywanie danych w lokalnej bazie danych przy użyciu pokoju.

    Nie musisz używać bazy danych do wdrażania repozytorium. Dostawca jest widoczny na zewnątrz jako zbiór tabel, podobnie jak w przypadku relacyjnej bazy danych, ale nie jest to wymagane w przypadku wewnętrznej implementacji dostawcy.

  • Android ma różne interfejsy API do przechowywania danych w postaci plików. Więcej informacji o miejscu na dane znajdziesz w artykule Miejsce na dane i pliki. Jeśli projektujesz dostawcę, który oferuje dane związane z multimediami, np. muzykę czy filmy, możesz zlecić jego dostawcy łączenie danych z tabel i plików.
  • W rzadkich przypadkach wdrożenie więcej niż jednego dostawcy treści w jednej aplikacji może być korzystne. Możesz na przykład udostępnić niektóre dane widżetowi za pomocą jednego dostawcy treści, a inny zestaw danych udostępnić innym aplikacjom.
  • Do pracy z danymi sieciowymi używaj klas w java.net i android.net. Możesz też synchronizować dane sieciowe z lokalnym magazynem danych, takim jak baza danych, a następnie oferować je w postaci tabel lub plików.

Uwaga: jeśli wprowadzasz w repozytorium zmianę, która nie jest zgodna wstecznie, musisz oznaczyć repozytorium nowym numerem wersji. Musisz też zwiększyć numer wersji aplikacji, która korzysta z nowego dostawcy treści. Dzięki tej zmianie zapobiega ona awarii systemu przy próbie ponownej instalacji aplikacji, która korzysta z niezgodnego dostawcy treści.

Uwagi na temat projektowania danych

Oto kilka wskazówek dotyczących projektowania struktury danych dostawcy:

  • Dane w tabeli muszą zawsze zawierać kolumnę „klucz podstawowy”, którą dostawca przechowuje jako unikalną wartość liczbową dla każdego wiersza. Za pomocą tej wartości możesz połączyć wiersz z powiązanymi wierszami w innych tabelach (używając jej jako „klucza obcego”). W tej kolumnie możesz użyć dowolnej nazwy, ale najlepszym wyborem jest BaseColumns._ID, ponieważ połączenie wyników zapytania dostawcy z kolumną ListView wymaga, by jedna z pobranych kolumn miała nazwę _ID.
  • Jeśli chcesz udostępnić obrazy bitmapowe lub inne bardzo duże fragmenty danych w postaci plików, zapisz dane w pliku, a potem podaj je pośrednio, zamiast przechowywać bezpośrednio w tabeli. Jeśli to zrobisz, musisz powiadomić użytkowników swojego dostawcy, że muszą użyć metody pliku ContentResolver, aby uzyskać dostęp do danych.
  • Używaj typu binarnego dużego obiektu (BLOB) do przechowywania danych, które różnią się rozmiarem lub mają różną strukturę. W kolumnie BLOB można na przykład zapisać bufor protokołu lub strukturę JSON.

    Za pomocą obiektu BLOB możesz też wdrożyć tabelę niezależną od schematu. W tabelach tego typu jako BLOB definiuje się kolumnę klucza podstawowego, kolumnę typu MIME i jedną lub więcej kolumn ogólnych. Znaczenie danych w kolumnach BLOB jest określane przez wartość w kolumnie Typ MIME. Pozwoli to przechowywać różne typy wierszy w tej samej tabeli. Przykładowa tabela „dane” dostawcy kontaktów ContactsContract.Data jest przykładem tabeli niezależnej od schematu.

Identyfikatory URI treści projektu

Identyfikator URI treści to identyfikator URI identyfikujący dane u dostawcy. Identyfikatory URI treści zawierają symboliczną nazwę całego dostawcy (jego autorytet) oraz nazwę wskazującą tabelę lub plik (ścieżkę). Opcjonalna część identyfikatora wskazuje pojedynczy wiersz w tabeli. Każda metoda dostępu do danych w ContentProvider ma identyfikator URI treści jako argument. Pozwoli Ci to określić tabelę, wiersz lub plik, do których chcesz uzyskać dostęp.

Informacje o identyfikatorach URI treści znajdziesz w artykule z podstawowymi informacjami o dostawcach treści.

Projektowanie organów

Dostawca ma zwykle jeden urząd, który pełni funkcję jego wewnętrznej nazwy Androida. Aby uniknąć konfliktów z innymi dostawcami, jako podstawę urzędu dostawcy podaj własność domeny internetowej (odwrotnie) jako podstawę uprawnień dostawcy. Ponieważ to zalecenie dotyczy również nazw pakietów na Androida, możesz zdefiniować urząd dostawcy jako rozszerzenie nazwy pakietu zawierającego dostawcę.

Jeśli na przykład nazwa Twojego pakietu na Androida to com.example.<appname>, przekaż swojemu dostawcy uprawnienia com.example.<appname>.provider.

Projektowanie struktury ścieżki

Deweloperzy zwykle tworzą identyfikatory URI treści na podstawie danych urzędu, dołączając ścieżki wskazujące poszczególne tabele. Jeśli na przykład masz 2 tabele, table1 i table2, możesz je połączyć z urzędem z poprzedniego przykładu, aby uzyskać identyfikatory URI treści com.example.<appname>.provider/table1 i com.example.<appname>.provider/table2. Ścieżki nie są ograniczone do 1 segmentu ani nie musi istnieć tabela dla każdego poziomu ścieżki.

Obsługa identyfikatorów URI treści

Zgodnie z konwencją dostawcy oferują dostęp do pojedynczego wiersza w tabeli, akceptując identyfikator URI treści z wartością identyfikatora wiersza na końcu identyfikatora URI. Poza tym zgodnie z konwencją dostawcy dopasowują wartość identyfikatora do kolumny _ID w tabeli i udzielają żądanego dostępu na podstawie pasującego wiersza.

Ta konwencja ułatwia spójne projektowanie aplikacji uzyskujących dostęp do dostawcy. Aplikacja wykonuje zapytanie do dostawcy i wyświetla wynik Cursor w ListView za pomocą CursorAdapter. Definicja obiektu CursorAdapter wymaga, aby jedna z kolumn w obiekcie Cursor miała wartość _ID

Następnie użytkownik wybiera w interfejsie jeden z wyświetlonych wierszy, by przejrzeć lub zmodyfikować dane. Aplikacja pobiera odpowiedni wiersz z Cursor zawierającego ListView, pobiera wartość _ID dla tego wiersza, dołącza ją do identyfikatora URI treści i wysyła żądanie dostępu do dostawcy. Dostawca może następnie wykonać zapytanie lub modyfikację względem wiersza wybranego przez użytkownika.

Wzorce identyfikatora URI treści

Aby ułatwić wybór czynności wykonywanej w przypadku identyfikatora URI treści przychodzących, interfejs API dostawcy udostępnia klasę wygodę UriMatcher, która mapuje wzorce identyfikatora URI treści na wartości całkowite. Możesz użyć wartości liczb całkowitych w instrukcji switch, która wybiera odpowiednie działanie w odniesieniu do identyfikatora URI treści lub identyfikatorów URI pasujących do określonego wzorca.

Wzorzec identyfikatora URI treści pasuje do identyfikatorów URI treści przy użyciu symboli wieloznacznych:

  • * pasuje do dowolnego prawidłowego ciągu znaków o dowolnej długości.
  • # pasuje do ciągu znaków liczbowych o dowolnej długości.

Jako przykład projektowania i kodowania obsługi identyfikatorów URI treści weź pod uwagę dostawcę z urzędem com.example.app.provider, który rozpoznaje następujące identyfikatory URI treści wskazujące tabele:

  • content://com.example.app.provider/table1: tabela o nazwie table1.
  • content://com.example.app.provider/table2/dataset1: tabela o nazwie dataset1.
  • content://com.example.app.provider/table2/dataset2: tabela o nazwie dataset2.
  • content://com.example.app.provider/table3: tabela o nazwie table3.

Dostawca rozpoznaje też te identyfikatory URI treści, jeśli do nich dołączany jest identyfikator wiersza, np. content://com.example.app.provider/table3/1 dla wiersza oznaczonego przez 1 w tabeli table3.

Oto możliwe wzorce identyfikatora URI treści:

content://com.example.app.provider/*
Pasuje do dowolnego identyfikatora URI treści u dostawcy.
content://com.example.app.provider/table2/*
Pasuje do identyfikatora URI treści w tabelach dataset1 i dataset2, ale nie do identyfikatorów URI treści w przypadku table1 ani table3.
content://com.example.app.provider/table3/#
Dopasowuje identyfikator URI treści do pojedynczych wierszy w argumencie table3, np. content://com.example.app.provider/table3/6 do wiersza oznaczonego przez 6.

Poniższy fragment kodu pokazuje, jak działają metody dostępne w pliku UriMatcher. Ten kod obsługuje identyfikatory URI całej tabeli inaczej niż identyfikatory URI pojedynczego wiersza, używając wzorca identyfikatora URI treści content://<authority>/<path> w przypadku tabel i content://<authority>/<path>/<id> w przypadku pojedynczych wierszy.

Metoda addURI() mapuje autorytet i ścieżkę na wartość całkowitą. Metoda match() zwraca wartość całkowitą dla identyfikatora URI. Instrukcja switch umożliwia wykonanie zapytania dotyczącego całej tabeli lub pojedynczego rekordu.

Kotlin

private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
    /*
     * The calls to addURI() go here for all the content URI patterns that the provider
     * recognizes. For this snippet, only the calls for table 3 are shown.
     */

    /*
     * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
     * in the path.
     */
    addURI("com.example.app.provider", "table3", 1)

    /*
     * Sets the code for a single row to 2. In this case, the # wildcard is
     * used. content://com.example.app.provider/table3/3 matches, but
     * content://com.example.app.provider/table3 doesn't.
     */
    addURI("com.example.app.provider", "table3/#", 2)
}
...
class ExampleProvider : ContentProvider() {
    ...
    // Implements ContentProvider.query()
    override fun query(
            uri: Uri?,
            projection: Array<out String>?,
            selection: String?,
            selectionArgs: Array<out String>?,
            sortOrder: String?
    ): Cursor? {
        var localSortOrder: String = sortOrder ?: ""
        var localSelection: String = selection ?: ""
        when (sUriMatcher.match(uri)) {
            1 -> { // If the incoming URI was for all of table3
                if (localSortOrder.isEmpty()) {
                    localSortOrder = "_ID ASC"
                }
            }
            2 -> {  // If the incoming URI was for a single row
                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query.
                 */
                localSelection += "_ID ${uri?.lastPathSegment}"
            }
            else -> { // If the URI isn't recognized,
                // do some error handling here
            }
        }

        // Call the code to actually do the query
    }
}

Java

public class ExampleProvider extends ContentProvider {
...
    // Creates a UriMatcher object.
    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        /*
         * The calls to addURI() go here for all the content URI patterns that the provider
         * recognizes. For this snippet, only the calls for table 3 are shown.
         */

        /*
         * Sets the integer value for multiple rows in table 3 to one. No wildcard is used
         * in the path.
         */
        uriMatcher.addURI("com.example.app.provider", "table3", 1);

        /*
         * Sets the code for a single row to 2. In this case, the # wildcard is
         * used. content://com.example.app.provider/table3/3 matches, but
         * content://com.example.app.provider/table3 doesn't.
         */
        uriMatcher.addURI("com.example.app.provider", "table3/#", 2);
    }
...
    // Implements ContentProvider.query()
    public Cursor query(
        Uri uri,
        String[] projection,
        String selection,
        String[] selectionArgs,
        String sortOrder) {
...
        /*
         * Choose the table to query and a sort order based on the code returned for the incoming
         * URI. Here, too, only the statements for table 3 are shown.
         */
        switch (uriMatcher.match(uri)) {


            // If the incoming URI was for all of table3
            case 1:

                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
                break;

            // If the incoming URI was for a single row
            case 2:

                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query.
                 */
                selection = selection + "_ID = " + uri.getLastPathSegment();
                break;

            default:
            ...
                // If the URI isn't recognized, do some error handling here
        }
        // Call the code to actually do the query
    }

Inna klasa, ContentUris, zapewnia wygodne metody pracy z częścią identyfikatorów URI treści id. Klasy Uri i Uri.Builder zawierają wygodne metody analizy istniejących obiektów Uri i tworzenia nowych.

Implementowanie klasy ContentProvider

Instancja ContentProvider zarządza dostępem do uporządkowanych danych przez obsługę żądań z innych aplikacji. Wszystkie formy dostępu wywołują ostatecznie metodę ContentResolver, która następnie wywołuje konkretną metodę ContentProvider w celu uzyskania dostępu.

Wymagane metody

Klasa abstrakcyjna ContentProvider definiuje 6 metod abstrakcyjnych, które implementujesz w ramach konkretnego podklasy. Wszystkie te metody oprócz onCreate() są wywoływane przez aplikację kliencką, która próbuje uzyskać dostęp do dostawcy treści.

query()
Pobierz dane od dostawcy. Użyj argumentów, aby wybrać tabelę, której ma dotyczyć zapytanie, wiersze i kolumny, które mają zostać zwrócone, oraz kolejność sortowania wyników. Zwraca dane jako obiekt Cursor.
insert()
Wstaw nowy wiersz w wierszu dostawcy. Użyj argumentów, aby wybrać tabelę docelową i uzyskać wartości kolumn do użycia. Zwraca identyfikator URI treści dla nowo wstawionego wiersza.
update()
Zaktualizuj istniejące wiersze u dostawcy. Użyj argumentów, aby wybrać tabelę i wiersze do zaktualizowania oraz pobrać zaktualizowane wartości kolumn. Zwraca liczbę zaktualizowanych wierszy.
delete()
Usuń wiersze od dostawcy. Użyj argumentów, aby wybrać tabelę i wiersze do usunięcia. Zwraca liczbę usuniętych wierszy.
getType()
Zwraca typ MIME odpowiadający identyfikatorowi URI treści. Metodę tę opisano bardziej szczegółowo w sekcji Implementowanie typów MIME dostawcy treści.
onCreate()
Zainicjuj dostawcę. System Android wywołuje tę metodę natychmiast po utworzeniu dostawcy. Dostawca nie zostanie utworzony, dopóki obiekt ContentResolver nie spróbuje uzyskać do niego dostępu.

Te metody mają taki sam podpis jak metody ContentResolver o identycznych nazwach.

Podczas wdrażania tych metod musisz uwzględnić:

  • Wszystkie te metody oprócz metody onCreate() mogą być wywoływane przez wiele wątków jednocześnie, więc muszą być bezpieczne w przypadku wątków. Więcej informacji o wielu wątkach znajdziesz w omówieniu procesów i wątków.
  • Unikaj długich operacji w obszarze onCreate(). Odrocz zadania inicjowania do momentu, gdy będą faktycznie potrzebne. Więcej informacji na ten temat znajdziesz w sekcji o implementowaniu metody onCreate().
  • Chociaż musisz wdrożyć te metody, kod nie musi nic robić oprócz zwrócenia oczekiwanego typu danych. Możesz na przykład uniemożliwić innym aplikacjom wstawianie danych do niektórych tabel, ignorując wywołanie insert() i zwracając 0.

Zaimplementuj metodę query()

Metoda ContentProvider.query() musi zwrócić obiekt Cursor lub, jeśli się nie uda, wysłać Exception. Jeśli do przechowywania danych używasz bazy danych SQLite, możesz zwrócić metodę Cursor zwróconych przez jedną z metod query() klasy SQLiteDatabase.

Jeśli zapytanie nie pasuje do żadnego wiersza, zwraca instancję Cursor, której metoda getCount() zwraca 0. Zwróć wartość null tylko wtedy, gdy podczas procesu zapytania wystąpił błąd wewnętrzny.

Jeśli do przechowywania danych nie używasz bazy danych SQLite, użyj jednej z konkretnych podklasy klasy Cursor. Na przykład klasa MatrixCursor implementuje kursor, w którym każdy wiersz jest tablicą instancji Object. W przypadku tej klasy użyj polecenia addRow(), aby dodać nowy wiersz.

System Android musi być w stanie komunikować się z Exception przez granice procesów. Android może to zrobić w przypadku tych wyjątków, które są przydatne przy obsłudze błędów zapytania:

Wdrażanie metody insert()

Metoda insert() dodaje nowy wiersz do odpowiedniej tabeli przy użyciu wartości z argumentu ContentValues. Jeśli nazwy kolumny nie ma w argumencie ContentValues, możesz podać wartość domyślną w kodzie dostawcy lub w schemacie bazy danych.

Ta metoda zwraca identyfikator URI treści w nowym wierszu. Aby to utworzyć, dołącz klucz podstawowy nowego wiersza (zwykle wartość _ID) do identyfikatora URI treści tabeli za pomocą polecenia withAppendedId().

Zaimplementuj metodę delete()

Metoda delete() nie wymaga usuwania wierszy z magazynu danych. Jeśli z dostawcą korzystasz z adaptera synchronizacji, zamiast usuwać całkowicie skasowany wiersz, oznacz usunięty wiersz flagą. Adapter synchronizacji może sprawdzić usunięte wiersze i usunąć je z serwera przed usunięciem u dostawcy.

Wdrażanie metody update()

Metoda update() przyjmuje ten sam argument ContentValues używany przez insert() oraz te same argumenty selection i selectionArgs, które są używane przez delete() i ContentProvider.query(). Może to umożliwić ponowne użycie kodu między tymi metodami.

Wdrażanie metody onCreate()

System Android wywołuje onCreate() po uruchomieniu dostawcy. W tej metodzie wykonuj tylko szybko uruchomione zadania inicjowania i opóźniaj tworzenie bazy danych oraz wczytywanie danych do momentu, gdy dostawca faktycznie otrzyma żądanie tych danych. Jeśli wykonujesz długie zadania w aplikacji onCreate(), spowalniasz uruchamianie dostawcy. To z kolei spowalnia odpowiedzi dostawcy do innych aplikacji.

Te 2 fragmenty kodu ilustrują interakcję między ContentProvider.onCreate() a Room.databaseBuilder(). Pierwszy fragment kodu przedstawia implementację polecenia ContentProvider.onCreate(), w którym tworzy się obiekt bazy danych i tworzone są uchwyty dla obiektów dostępu do danych:

Kotlin

// Defines the database name
private const val DBNAME = "mydb"
...
class ExampleProvider : ContentProvider() {

    // Defines a handle to the Room database
    private lateinit var appDatabase: AppDatabase

    // Defines a Data Access Object to perform the database operations
    private var userDao: UserDao? = null

    override fun onCreate(): Boolean {

        // Creates a new database object
        appDatabase = Room.databaseBuilder(context, AppDatabase::class.java, DBNAME).build()

        // Gets a Data Access Object to perform the database operations
        userDao = appDatabase.userDao

        return true
    }
    ...
    // Implements the provider's insert method
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc.
    }
}

Java

public class ExampleProvider extends ContentProvider

    // Defines a handle to the Room database
    private AppDatabase appDatabase;

    // Defines a Data Access Object to perform the database operations
    private UserDao userDao;

    // Defines the database name
    private static final String DBNAME = "mydb";

    public boolean onCreate() {

        // Creates a new database object
        appDatabase = Room.databaseBuilder(getContext(), AppDatabase.class, DBNAME).build();

        // Gets a Data Access Object to perform the database operations
        userDao = appDatabase.getUserDao();

        return true;
    }
    ...
    // Implements the provider's insert method
    public Cursor insert(Uri uri, ContentValues values) {
        // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc.
    }
}

Wdrażanie typów MIME komponentu ContentProvider

Klasa ContentProvider ma 2 metody zwracania typów MIME:

getType()
Jedna z wymaganych metod wdrażanych przez Ciebie w przypadku dowolnego dostawcy.
getStreamTypes()
Metoda, którą musisz wdrożyć, jeśli dostawca oferuje pliki.

Typy MIME tabel

Metoda getType() zwraca String w formacie MIME, który opisuje typ danych zwracanych przez argument identyfikatora URI treści. Argument Uri może być wzorem, a nie określonym identyfikatorem URI. W takim przypadku zwraca typ danych powiązanych z identyfikatorami URI treści pasującymi do wzorca.

W przypadku typowych typów danych, takich jak tekst, HTML czy JPEG, funkcja getType() zwraca standardowy typ MIME. Pełną listę tych standardowych typów znajdziesz na stronie IANA MIME Media Types.

W przypadku identyfikatorów URI treści wskazujących wiersz lub wiersze danych w tabeli funkcja getType() zwraca typ MIME w używanym przez dostawcę formacie MIME Androida:

  • Część typu: vnd
  • Część podtypu:
    • Jeśli wzorzec identyfikatora URI dotyczy pojedynczego wiersza: android.cursor.item/
    • Jeśli wzorzec identyfikatora URI dotyczy więcej niż jednego wiersza: android.cursor.dir/
  • Część u dostawcy: vnd.<name>.<type>

    Ty dostarczasz <name> i <type>. Wartość <name> jest globalnie unikalna, a wartość <type> jest unikalna dla odpowiedniego wzorca identyfikatora URI. Dobrym rozwiązaniem w przypadku <name> jest nazwa Twojej firmy lub część nazwy pakietu aplikacji na Androida. Dobrym wyborem w przypadku <type> jest ciąg znaków, który identyfikuje tabelę powiązaną z identyfikatorem URI.

Jeśli na przykład dostawca ma uprawnienia com.example.app.provider i udostępnia tabelę o nazwie table1, typ MIME dla wielu wierszy w tabeli table1 to:

vnd.android.cursor.dir/vnd.com.example.provider.table1

Typ MIME pojedynczego wiersza wartości table1 to:

vnd.android.cursor.item/vnd.com.example.provider.table1

Typy MIME plików

Jeśli dostawca oferuje pliki, wdróż getStreamTypes(). Ta metoda zwraca tablicę String typów MIME dla plików, które dostawca może zwrócić w przypadku danego identyfikatora URI treści. Przefiltruj oferowane typy MIME według argumentu filtra typu MIME, aby zwracane były tylko te typy MIME, które klient chce obsługiwać.

Weźmy na przykład dostawcę, który oferuje zdjęcia w postaci plików w formacie JPG, PNG i GIF. Jeśli aplikacja wywołuje ContentResolver.getStreamTypes() z ciągiem filtra image/*, w przypadku obiektu „image” (obraz) metoda ContentProvider.getStreamTypes() zwraca tablicę:

{ "image/jpeg", "image/png", "image/gif"}

Jeśli aplikacja potrzebuje tylko plików JPG, może wywołać ContentResolver.getStreamTypes() z ciągiem filtra *\/jpeg, a getStreamTypes() zwróci:

{"image/jpeg"}

Jeśli dostawca nie oferuje żadnego z typów MIME wymaganych w ciągu filtra, getStreamTypes() zwraca wartość null.

Wdróż klasę umowy

Klasa umowy to klasa public final, która zawiera stałe definicje identyfikatorów URI, nazw kolumn, typów MIME i inne metadane odnoszące się do dostawcy. Klasa określa umowę między dostawcą a innymi aplikacjami, zapewniając prawidłowy dostęp do dostawcy, nawet jeśli zmienią się rzeczywiste wartości identyfikatorów URI, nazwy kolumn itp.

Klasa kontraktowa pomaga również programistom, ponieważ zwykle ma mnemotechniczne nazwy stałych, więc deweloperzy będą rzadziej używać nieprawidłowych wartości w nazwach kolumn czy identyfikatorach URI. Ponieważ jest to klasa, może zawierać dokumentację Javadoc. Zintegrowane środowiska programistyczne, takie jak Android Studio, mogą automatycznie uzupełniać stałe nazwy z klasy umowy i wyświetlać Javadoc dla stałych.

Deweloperzy nie mają dostępu do pliku klasy umowy z Twojej aplikacji, ale mogą statycznie skompilować go do swojej aplikacji z dostarczonego przez Ciebie pliku JAR.

Przykładami klas kontraktu są klasa ContactsContract i jej zagnieżdżone klasy.

Wdrażanie uprawnień dostawcy treści

Uprawnienia i dostęp do wszystkich aspektów systemu Android zostały szczegółowo opisane we wskazówkach dotyczących bezpieczeństwa. W artykule Przechowywanie danych i plików znajdziesz też informacje o zabezpieczeniach i uprawnieniach obowiązujących w przypadku różnych typów pamięci. Oto najważniejsze kwestie:

  • Domyślnie pliki danych przechowywane w pamięci wewnętrznej urządzenia są prywatne dla aplikacji i dostawcy.
  • Utworzone przez Ciebie bazy danych SQLiteDatabase są prywatne dla aplikacji i dostawcy.
  • Domyślnie pliki danych zapisywane w pamięci zewnętrznej są publiczne i dostępne do odczytu na całym świecie. Nie możesz ograniczyć dostępu do plików w pamięci zewnętrznej, korzystając z usług dostawcy treści, ponieważ inne aplikacje mogą używać innych wywołań interfejsu API do odczytywania i zapisywania tych plików.
  • Metoda ta wymaga otwierania lub tworzenia plików lub baz danych SQLite w pamięci wewnętrznej urządzenia, co może zapewnić wszystkim innym aplikacjom dostęp zarówno do odczytu, jak i do zapisu. Jeśli jako repozytorium dostawcy używasz wewnętrznego pliku lub bazy danych i przyznasz mu dostęp „dostępny do odczytu na całym świecie” lub „możliwy do zapisu przez świat”, uprawnienia ustawione dla dostawcy w jego pliku manifestu nie będą chronić Twoich danych. Domyślny dostęp do plików i baz danych w pamięci wewnętrznej to „prywatny”. Nie zmieniaj tego ustawienia w przypadku repozytorium dostawcy.

Jeśli chcesz używać uprawnień dostawcy treści do kontrolowania dostępu do swoich danych, zapisz dane w plikach wewnętrznych, bazach danych SQLite lub w chmurze, np. na serwerze zdalnym, oraz zachowaj prywatność plików i baz danych w aplikacji.

Wdróż uprawnienia

Domyślnie wszystkie aplikacje mogą odczytywać i zapisywać dane od Twojego dostawcy, nawet jeśli dane bazowe są prywatne. Domyślnie Twój dostawca nie ma ustawionych uprawnień. Aby to zmienić, określ uprawnienia dostawcy w pliku manifestu za pomocą atrybutów lub elementów podrzędnych elementu <provider>. Możesz ustawić uprawnienia, które dotyczą całego dostawcy, wybranych tabel, określonych rekordów lub wszystkich trzech.

Uprawnienia dostawcy określasz za pomocą co najmniej 1 elementu <permission> w pliku manifestu. Aby uprawnienie było unikalne dla Twojego dostawcy, użyj dla atrybutu android:name określania zakresu jak w języku Java. Możesz na przykład nazwać uprawnienie do odczytu com.example.app.provider.permission.READ_PROVIDER.

Poniższa lista opisuje zakres uprawnień dostawcy, począwszy od uprawnień, które dotyczą całego dostawcy, po bardziej szczegółowe. Uprawnienia o większej szczegółowości mają pierwszeństwo przed uprawnieniami o szerszym zakresie.

Pojedyncze uprawnienie na poziomie dostawcy do odczytu i zapisu
Jedno uprawnienie, które kontroluje dostęp do całego dostawcy z możliwością odczytu i zapisu, określone za pomocą atrybutu android:permission elementu <provider>.
Oddzielne uprawnienia na poziomie dostawcy do odczytu i zapisu
Uprawnienia do odczytu i zapisu dla całego dostawcy. Określasz je za pomocą atrybutów android:readPermission i android:writePermission elementu <provider>. Mają one pierwszeństwo przed uprawnieniami wymaganymi przez zasadę android:permission.
Uprawnienia na poziomie ścieżki
Uprawnienia do odczytu, zapisu lub odczytu i zapisu dla identyfikatora URI treści w systemie dostawcy. Każdy identyfikator URI, który chcesz kontrolować, określa się za pomocą elementu podrzędnego <path-permission> elementu <provider>. Dla każdego identyfikatora URI treści możesz określić uprawnienia do odczytu i zapisu, do odczytu, zapisu lub wszystkie trzy. Uprawnienia do odczytu i zapisu mają pierwszeństwo przed uprawnieniami do odczytu i zapisu. Poza tym uprawnienia na poziomie ścieżki mają pierwszeństwo przed uprawnieniami na poziomie dostawcy.
Uprawnienia tymczasowe
Poziom uprawnień, który przyznaje tymczasowy dostęp do aplikacji, nawet jeśli nie ma ona uprawnień, które są zwykle wymagane. Funkcja dostępu tymczasowego zmniejsza liczbę uprawnień, o które może poprosić aplikacja w pliku manifestu. Gdy włączysz uprawnienia tymczasowe, jedynymi aplikacjami, które wymagają trwałych uprawnień u Twojego dostawcy, będą te, które mają stały dostęp do wszystkich Twoich danych.

Na przykład weź pod uwagę potrzebne uprawnienia, jeśli wdrażasz dostawcę poczty e-mail i aplikację i chcesz, aby zewnętrzna aplikacja do przeglądania obrazów mogła wyświetlać załączniki ze zdjęciami od dostawcy. Aby przyznać przeglądarce obrazów niezbędny dostęp bez konieczności uzyskania uprawnień, możesz ustawić tymczasowe uprawnienia dostępu do identyfikatorów URI treści w przypadku zdjęć.

Zaprojektuj aplikację do poczty e-mail w taki sposób, aby za każdym razem, gdy użytkownik chciał wyświetlić zdjęcie, aplikacja wysyłała do przeglądarki obrazów intencję zawierającą identyfikator URI zawartości zdjęcia oraz flagi uprawnień. Przeglądarka obrazów może wysłać do dostawcy poczty e-mail prośbę o pobranie zdjęcia, mimo że nie ma zwykłych uprawnień do odczytu u dostawcy.

Aby włączyć uprawnienia tymczasowe, ustaw atrybut android:grantUriPermissions elementu <provider> lub dodaj co najmniej 1 element podrzędny <grant-uri-permission> do elementu <provider>. Wywołuj Context.revokeUriPermission() za każdym razem, gdy usuniesz obsługę identyfikatora URI treści powiązanego z tymczasowym uprawnieniem u dostawcy.

Wartość atrybutu określa, w jakim stopniu informacje od dostawcy są dostępne. Jeśli atrybut ma wartość "true", system przyznaje tymczasowe uprawnienia całemu dostawcy, zastępując wszystkie inne uprawnienia wymagane przez uprawnienia na poziomie dostawcy lub ścieżki.

Jeśli ta flaga jest ustawiona na "false", dodaj elementy podrzędne (<grant-uri-permission>) do elementu <provider>. Każdy element podrzędny określa identyfikator URI treści lub identyfikatory URI, do których przyznawany jest tymczasowy dostęp.

Aby można było przekazać aplikacji tymczasowy dostęp, intencja musi zawierać flagę FLAG_GRANT_READ_URI_PERMISSION, flagę FLAG_GRANT_WRITE_URI_PERMISSION lub obie. Są one ustawiane za pomocą metody setFlags().

Jeśli atrybut android:grantUriPermissions nie istnieje, przyjmuje się, że ma on wartość "false".

Element <provider>

Tak jak w przypadku komponentów Activity i Service, w pliku manifestu aplikacji zdefiniowano podklasę ContentProvider za pomocą elementu <provider>. Android pobiera z elementu te informacje:

Uprawnienia (android:authorities)
Symboliczne nazwy, które identyfikują całego dostawcę w systemie. Ten atrybut został szczegółowo opisany w sekcji Identyfikatory URI treści projektu.
Nazwa klasy dostawcy (android:name)
Klasa, która implementuje ContentProvider. Tę klasę szczegółowo omówiono w sekcji Implementowanie klasy ContentProvider.
Uprawnienia
Atrybuty określające uprawnienia, które inne aplikacje muszą mieć, aby uzyskać dostęp do danych dostawcy:

Uprawnienia i odpowiadające im atrybuty zostały szczegółowo opisane w sekcji Wdrażanie uprawnień dostawcy treści.

Atrybuty uruchamiania i sterowania
Te atrybuty określają, w jaki sposób i kiedy system Android uruchamia dostawcę, charakterystykę procesu dostawcy oraz inne ustawienia środowiska wykonawczego:
  • android:enabled: flaga umożliwiająca systemowi uruchomienie dostawcy
  • android:exported: flaga umożliwiająca innym aplikacjom korzystanie z tego dostawcy
  • android:initOrder: kolejność, w jakiej został uruchomiony dostawca względem innych dostawców w tym samym procesie
  • android:multiProcess: flaga umożliwiająca systemowi uruchomienie dostawcy w tym samym procesie co klient wywołujący
  • android:process: nazwa procesu, w którym działa dostawca
  • android:syncable: flaga wskazująca, że dane dostawcy mają zostać zsynchronizowane z danymi na serwerze.

Pełną dokumentację tych atrybutów znajdziesz w przewodniku po elemencie <provider>.

Atrybuty informacyjne
Opcjonalna ikona i etykieta dla dostawcy:
  • android:icon: rysowalny zasób zawierający ikonę dostawcy. Ikona pojawi się obok etykiety dostawcy na liście aplikacji w sekcji Ustawienia > Aplikacje > Wszystkie.
  • android:label: etykieta informacyjna opisująca dostawcę, jego dane lub oba te elementy. Pojawi się ona na liście aplikacji w sekcji Ustawienia > Aplikacje > Wszystkie.

Pełną dokumentację tych atrybutów znajdziesz w przewodniku po elemencie <provider>.

Uwaga: jeśli kierujesz aplikację na Androida 11 lub nowszego, dodatkowe informacje o konfiguracji znajdziesz w dokumentacji dotyczącej widoczności pakietów.

Intencje i dostęp do danych

Aplikacje mogą uzyskiwać dostęp do dostawcy treści pośrednio za pomocą Intent. Aplikacja nie wywołuje żadnej z metod ContentResolver ani ContentProvider. Zamiast tego wysyła intencję rozpoczynającą działanie, która często jest częścią własnej aplikacji dostawcy. Działanie w miejscu docelowym odpowiada za pobieranie i wyświetlanie danych w swoim interfejsie użytkownika.

W zależności od działania w intencji działanie miejsca docelowego może też prosić użytkownika o wprowadzenie zmian w danych dostawcy. Intencja może też zawierać dane „wyodrębnione”, które działanie docelowe wyświetla w interfejsie. Użytkownik może potem zmienić te dane, zanim użyje ich do zmodyfikowania danych u dostawcy.

Dostęp do intencji pomaga w integralności danych. Dostawca może zależeć od tego, czy dane będą wstawiane, aktualizowane i usuwane zgodnie ze ściśle zdefiniowaną logiką biznesową. W takim przypadku zezwolenie innym aplikacjom na bezpośrednie modyfikowanie danych może doprowadzić do wygenerowania nieprawidłowych danych.

Jeśli chcesz, aby deweloperzy korzystali z dostępu do intencji, dokładnie udokumentuj to działanie. Wyjaśnij, dlaczego dostęp do intencji przez interfejs aplikacji jest lepszy od modyfikowania danych za pomocą kodu.

Obsługa intencji przychodzącej, która chce zmodyfikować dane dostawcy, nie różni się od obsługi innych intencji. Więcej informacji o korzystaniu z intencji znajdziesz w artykule Intencje i filtry intencji.

Dodatkowe informacje znajdziesz w artykule Omówienie dostawcy kalendarza.