Utwórz dostawcę treści

Dostawca treści zarządza dostępem do centralnego repozytorium danych. Dostawca wdrażasz jako jedną lub więcej klas w aplikacji na Androida razem z elementami w pliku manifestu. Jedna z klas implementuje podklasę klasy ContentProvider, która stanowi interfejs między Twoim dostawcą a innymi aplikacjami.

Chociaż dostawcy treści mają udostępniać dane innym aplikacjom, możesz mieć w swojej aplikacji działania, które pozwolą użytkownikom wysyłać zapytania i modyfikować dane zarządzane przez dostawcę.

Ta strona zawiera podstawowy proces tworzenia dostawcy treści oraz listę interfejsów API do użycia.

Zanim zaczniesz tworzyć

Zanim utworzysz dostawcę, weź pod uwagę te kwestie:

  • Zdecyduj, czy potrzebujesz dostawcy treści. Jeśli chcesz udostępnić co najmniej jedną z tych funkcji, musisz utworzyć dostawcę treści:
    • chcesz zaoferować złożone dane lub pliki innym aplikacjom;
    • Chcesz umożliwić użytkownikom kopiowanie złożonych danych z 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 korzystać z usług dostawcy, aby korzystać z baz danych lub innych typów pamięci trwałej, jeśli jest ona wykorzystywana wyłącznie w Twojej aplikacji i nie potrzebujesz wymienionych wcześniej funkcji. Zamiast tego możesz korzystać z jednego z systemów pamięci masowej opisanych w omówieniu miejsca na dane i pliki.

  • Zapoznaj się z podstawowymi informacjami o dostawcach treści, aby dowiedzieć się więcej o dostawcach i sposobach ich działania.

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

  1. Zaprojektuj nieprzetworzone miejsce 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 prywatnym obszarze aplikacji. W odpowiedzi na żądanie pliku z innej aplikacji dostawca może zaoferować nick dla tego pliku.
    „Uporządkowane” dane
    Dane, które zwykle trafiają do bazy danych, tablicy lub podobnej struktury. Przechowuj dane w formie zgodnej z tabelami wierszy i kolumn. Wiersz reprezentuje element, np. osobę lub produkt w zasobach reklamowych. Kolumna zawiera niektóre dane dotyczące encji, np. imię i nazwisko osoby lub cenę produktu. Typowym sposobem przechowywania danych tego typu jest baza danych SQLite, ale możesz użyć dowolnego typu pamięci trwałej. Więcej informacji o typach pamięci masowej dostępnych w systemie Android znajdziesz w sekcji Projektowanie miejsca na dane.
  2. Zdefiniuj konkretną implementację klasy ContentProvider i jej wymaganych metod. Ta klasa stanowi interfejs między Twoimi danymi a resztą systemu Android. Więcej informacji o tej klasie znajdziesz w sekcji o implementowaniu klasy ContentProvider.
  3. Określ ciąg tekstowy z uprawnieniami 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ć te zajęcia innym programistom. Więcej informacji o identyfikatorach URI treści znajdziesz w sekcji Projektowanie identyfikatorów URI treści. Więcej informacji o intencji 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.

Pamięć projektu

Dostawca treści to interfejs do danych zapisanych w uporządkowanym formacie. Zanim utworzysz interfejs, zdecyduj, jak chcesz przechowywać dane. Możesz przechowywać dane w dowolnej formie, a następnie zaprojektować interfejs do odczytywania i zapisywania danych zgodnie z potrzebami.

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

  • Jeśli korzystasz z uporządkowanych danych, rozważ skorzystanie z relacyjnej bazy danych (np. SQLite) lub z nierelacyjnego magazynu danych, takiego jak LevelDB. Jeśli korzystasz z nieuporządkowanych danych, takich jak multimedia, obrazy lub filmy, rozważ przechowywanie ich w postaci plików. Możesz mieszać i dopasowywać kilka różnych typów pamięci masowej i w razie potrzeby udostępniać je 1 dostawcy treści.
  • System Android może wchodzić w interakcje z biblioteką trwałości pokoju, która zapewnia dostęp do interfejsu API bazy danych SQLite wykorzystywanego przez dostawców Androida do przechowywania danych zorientowanych na tabele. Aby utworzyć bazę danych przy użyciu tej biblioteki, utwórz instancję podklasy RoomDatabase zgodnie z opisem w sekcji Zapisywanie danych w lokalnej bazie danych za pomocą funkcji Room.

    Aby wdrożyć repozytorium, nie musisz używać bazy danych. Dostawca pojawia się na zewnątrz jako zbiór tabel podobny do relacyjnej bazy danych, ale nie jest to wymagane do jego wewnętrznej implementacji.

  • Do przechowywania danych plików Android ma różne interfejsy API zorientowane na pliki. Więcej informacji o miejscu na pliki znajdziesz w artykule Omówienie miejsca na dane i pliki. Jeśli projektujesz dostawcę, który oferuje dane związane z multimediami, np. muzykę lub filmy, możesz skorzystać z usług dostawcy, który łączy dane tabel z plikami.
  • W rzadkich przypadkach może się okazać, że w przypadku jednej aplikacji korzystne może się okazać wdrożenie więcej niż 1 dostawcy treści. Możesz na przykład udostępnić niektóre dane widżetowi, korzystając z usług 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ż zsynchronizować dane sieciowe z lokalnym magazynem danych, takim jak baza danych, a następnie udostępnić je w postaci tabel lub plików.

Uwaga: jeśli wprowadzisz w repozytorium zmianę, która nie jest zgodna wstecznie, musisz oznaczyć repozytorium nowym numerem wersji. Musisz też zwiększyć numer wersji aplikacji, w której implementuje się nowego dostawcę treści. Dzięki tej zmianie zmiana na starszą wersję systemu nie spowoduje awarii systemu podczas próby ponownego zainstalowania aplikacji, której dostawca treści nie jest zgodny.

Uwagi na temat projektowania danych

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

  • Dane w tabeli muszą zawsze mieć kolumnę „klucz podstawowy”, którą dostawca zachowuje jako unikalną wartość liczbową w każdym wierszu. Możesz użyć tej wartości, aby połączyć wiersz z powiązanymi wierszami w innych tabelach (jako „klucz obcy”). Chociaż możesz użyć dowolnej nazwy dla tej kolumny, najlepszym rozwiązaniem jest użycie właściwości BaseColumns._ID, ponieważ połączenie wyników zapytania dostawcy z kolumną ListView wymaga, aby jedna z pobranych kolumn miała nazwę _ID.
  • Jeśli chcesz udostępnić obrazy bitmapowe lub inne bardzo duże porcje danych związanych z plikami, zapisz je w pliku, a następnie podaj pośrednio, zamiast przechowywać je bezpośrednio w tabeli. Jeśli to zrobisz, musisz poinformować użytkowników swojego dostawcy, że aby uzyskać dostęp do danych, muszą użyć metody pliku ContentResolver.
  • Typ danych BLOB z dużymi obiektami służy do przechowywania danych o różnym rozmiarze lub o zróżnicowanej strukturze. W kolumnie BLOB możesz na przykład przechowywać bufor protokołu lub strukturę JSON.

    Możesz też użyć obiektu BLOB do zaimplementowania tabeli niezależnej od schematu. W tabeli tego typu definiuje się kolumnę klucza podstawowego, kolumnę typu MIME i co najmniej jedną kolumnę ogólną jako obiekt BLOB. Znaczenie danych w kolumnach BLOB jest określone przez wartość w kolumnie typu MIME. Dzięki temu w tej samej tabeli możesz przechowywać różne typy wierszy. Tabela „dane” dostawcy kontaktów ContactsContract.Data to przykład tabeli niezależnej od schematu.

Identyfikatory URI treści projektu

Identyfikator URI treści to identyfikator URI, który określa dane dostawcy. Identyfikatory URI treści obejmują symboliczną nazwę całego dostawcy (jej uprawnienie) i nazwę, która wskazuje tabelę lub plik (ścieżka). Opcjonalna część identyfikatora wskazuje pojedynczy wiersz w tabeli. Każda metoda dostępu do danych ContentProvider ma jako argument identyfikator URI treści. Pozwala 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 instytucji

Dostawca ma zwykle pojedynczy urząd, który służy jako jego wewnętrzna nazwa Androida. Aby uniknąć konfliktów z innymi dostawcami, użyj własności domeny internetowej (odwróconie) jako podstawy uprawnień dostawcy. Ta rekomendacja dotyczy również nazw pakietów na Androida, więc możesz zdefiniować swoje urząd dostawcy jako rozszerzenie nazwy pakietu zawierającego dostawcę.

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

Projektowanie struktury ścieżki

Deweloperzy zwykle tworzą identyfikatory URI treści z urzędu, dołączając ścieżki wskazujące poszczególne tabele. Jeśli np. masz 2 tabele – table1 i table2, możesz je połączyć z uprawnieniami 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 jednego segmentu i nie musi być tabela dla każdego poziomu ścieżki.

Obsługa identyfikatorów URI treści

Zgodnie z konwencją dostawcy oferują dostęp do 1 wiersza w tabeli, akceptując identyfikator URI treści z wartością identyfikatora wiersza na końcu identyfikatora URI. Zgodnie z konwencją dostawcy dopasowują wartość identyfikatora do kolumny _ID tabeli i przyznają żądany dostęp do pasującego wiersza.

Dzięki tej konwencji można korzystać z typowego wzorca projektowego dla aplikacji uzyskujących dostęp do dostawcy. Aplikacja wysyła zapytanie do dostawcy i wyświetla wynik Cursor w ListView przy użyciu CursorAdapter. Definicja obiektu CursorAdapter wymaga, aby jedna z kolumn Cursor miała wartość _ID

Następnie wybiera jeden z wyświetlanych wierszy w interfejsie, aby obejrzeć lub zmodyfikować dane. Aplikacja otrzymuje odpowiedni wiersz z Cursor kopii zapasowej ListView, otrzymuje wartość _ID w tym wierszu, dołącza ją do identyfikatora URI treści i wysyła żądanie dostępu do dostawcy. Dostawca może wykonać zapytanie lub modyfikować dokładnie wiersz wybrany przez użytkownika.

Wzorce URI treści

Aby ułatwić wybór działania wykonywanego w przypadku przychodzącego identyfikatora URI treści, interfejs API dostawcy zawiera klasę wygody UriMatcher, która mapuje wzorce URI treści na wartości całkowite. Możesz użyć wartości całkowitych w instrukcji switch, która wskazuje działanie związane z identyfikatorem URI treści lub identyfikatorem URI zgodnym z konkretnym wzorcem.

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

  • * dopasowuje ciąg znaków o dowolnej długości.
  • # odpowiada ciągowi znaków o dowolnej długości.

Przykładem może być projektowanie i kodowanie obsługi identyfikatorów URI treści. Rozważmy na przykład dostawcę z uprawnieniem com.example.app.provider, który rozpoznaje te 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 mają do nich dołączony identyfikator wiersza, na przykład content://com.example.app.provider/table3/1 w wierszu wskazanym przez 1 w tabeli table3.

Oto możliwe wzorce URI treści:

content://com.example.app.provider/*
Pasuje do dowolnego identyfikatora URI treści u dostawcy.
content://com.example.app.provider/table2/*
Dopasowuje identyfikator URI treści do tabel dataset1 i dataset2, ale nie pasuje do identyfikatorów URI treści w przypadku table1 ani table3.
content://com.example.app.provider/table3/#
Dopasowuje identyfikator URI treści dla pojedynczych wierszy w tabeli table3, np. content://com.example.app.provider/table3/6 w wierszu wskazanym przez parametr 6.

Ten fragment kodu pokazuje, jak działają metody w UriMatcher. Ten kod obsługuje identyfikatory URI całej tabeli inaczej niż identyfikatory URI dla pojedynczych wierszy. W tym celu używany jest wzorzec identyfikatora URI treści content://<authority>/<path> dla tabel i content://<authority>/<path>/<id> dla pojedynczych wierszy.

Metoda addURI() mapuje urząd i ścieżkę na wartość całkowitą. Metoda match() zwraca wartość całkowitą dla identyfikatora URI. Instrukcja switch służy do wyboru między wysłaniem zapytania dotyczącego całej tabeli a wysłaniem zapytania dotyczącego 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, udostępnia wygodne metody pracy z częścią id identyfikatorów URI treści. Klasy Uri i Uri.Builder obejmują wygodne metody analizy istniejących obiektów Uri i tworzenia nowych.

Implementowanie klasy ContentProvider

Instancja ContentProvider zarządza dostępem do uporządkowanego zbioru danych, obsługując żądania z innych aplikacji. Wszystkie formy dostępu ostatecznie wywołują metodę ContentResolver, która wywołuje konkretną metodę ContentProvider, aby uzyskać dostęp.

Wymagane metody

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

query()
Pobierz dane od dostawcy. Użyj argumentów, aby wybrać tabelę, której ma dotyczyć zapytanie, wiersze i kolumny do zwrócenia oraz kolejność sortowania wyników. Zwraca dane jako obiekt Cursor.
insert()
Wstaw nowy wiersz do pola dostawcy. Za pomocą argumentów wybierz tabelę docelową i pobierz wartości kolumn do użycia. Zwraca identyfikator URI treści dla nowo wstawionego wiersza.
update()
Zaktualizuj dotychczasowe wiersze u dostawcy. Za pomocą argumentów wybierz tabelę i wiersze do aktualizacji oraz aby 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. Ta metoda została szczegółowo opisana w sekcji Implementowanie typów MIME dostawców 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 identycznej nazwie.

Implementacja tych metod musi uwzględniać następujące elementy:

  • Wszystkie te metody (oprócz metody onCreate()) mogą być wywoływane przez wiele wątków naraz, więc muszą być one bezpieczne w wątku. Więcej informacji o wielu wątkach znajdziesz w omówieniu procesów i wątków.
  • Unikaj długich operacji w onCreate(). Odkładaj zadania inicjowania do momentu, aż będą faktycznie potrzebne. Szczegółowo omawiamy to w sekcji dotyczącej implementowania metody onCreate().
  • Chociaż musisz wdrożyć te metody, Twój kod nie musi nic robić poza zwracaniem oczekiwanego typu danych. Możesz na przykład uniemożliwić innym aplikacjom wstawianie danych do niektórych tabel, ignorując wywołanie funkcji insert() i zwracając wartość 0.

Implementacja metody query()

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

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

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

System Android musi być w stanie przekazać interfejs Exception poza granice procesów. Android może to zrobić w przypadku tych wyjątków, które przydają się w obsłudze błędów zapytań:

Implementowanie metody insert()

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

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

Implementowanie metody delete()

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

Wdrażanie metody update()

Metoda update() przyjmuje ten sam argument ContentValues używany przez insert() oraz te same argumenty selection i selectionArgs, których używają delete() i ContentProvider.query(). Dzięki temu możesz ponownie wykorzystać kod między tymi metodami.

Implementacja metody onCreate()

Podczas uruchamiania dostawcy system Android wywołuje onCreate(). W ramach tej metody możesz wykonywać tylko szybkie zadania inicjowania oraz opóźniać tworzenie i wczytywanie bazy danych, dopóki dostawca nie otrzyma żądania danych. Jeśli wykonujesz długie zadania w onCreate(), spowalnia to uruchamianie dostawcy. To z kolei spowalnia odpowiedzi dostawcy na inne aplikacje.

Te 2 fragmenty kodu ilustrują interakcję między ContentProvider.onCreate() a Room.databaseBuilder(). Pierwszy fragment kodu pokazuje implementację ContentProvider.onCreate() w miejscu, w którym tworzony jest obiekt bazy danych i są obsługiwane obiekty 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 ContentProvider

Klasa ContentProvider ma 2 metody zwracania typów MIME:

getType()
Jedna z wymaganych metod, którą implementujesz w przypadku każdego dostawcy.
getStreamTypes()
Metoda, którą zwykle trzeba wdrożyć, jeśli dostawca udostępnia 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ć wzorcem, a nie konkretnym identyfikatorem URI. W tym przypadku zwraca typ danych powiązanych z identyfikatorami URI treści, które pasują do wzorca.

W przypadku typowych typów danych, takich jak tekst, HTML lub JPEG, getType() zwraca standardowy typ MIME tych danych. Pełną listę tych standardowych typów mediów można znaleźć na stronie IANA MIME Media Type (Typy mediów MIME IANA).

W przypadku identyfikatorów URI treści wskazujących wiersz lub wiersze danych w tabeli getType() zwraca typ MIME w formacie MIME specyficznym dla dostawcy Androida:

  • Wpisz część: 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ęść specyficzna dla dostawcy: vnd.<name>.<type>

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

Jeśli na przykład uprawnienia dostawcy to com.example.app.provider i wyświetlana jest tabela o nazwie table1, typ MIME wielu wierszy w tabeli table1 to:

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

W przypadku pojedynczego wiersza pola table1 typ MIME to:

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

Typy MIME plików

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

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

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

Jeśli aplikacja interesuje tylko pliki JPG, może wywołać funkcję ContentResolver.getStreamTypes() z ciągiem filtra *\/jpeg, a getStreamTypes() zwraca:

{"image/jpeg"}

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

Wdrażanie klasy umowy

Klasa umowy to klasa public final zawierająca stałe definicje identyfikatorów URI, nazw kolumn, typów MIME i innych metadanych dotyczących dostawcy. Klasa nawiązuje umowę między dostawcą a innymi aplikacjami, zapewniając poprawny dostęp do dostawcy nawet w przypadku zmiany rzeczywistych wartości identyfikatorów URI, nazw kolumn itp.

Klasa kontraktowa pomaga programistom, ponieważ zwykle ma mnemotechniczne nazwy stałych wartości. Dzięki temu zmniejszają oni prawdopodobieństwo użycia nieprawidłowych wartości w nazwach kolumn i identyfikatorach URI. Ponieważ jest to klasa, może zawierać dokumentację Javadoc. Zintegrowane środowiska programistyczne, takie jak Android Studio, mogą automatycznie uzupełniać nazwy stałe z klasy umowy i wyświetlać dokument Javadoc dla stałych.

Programiści nie mają dostępu do pliku klasy klasy umowy z aplikacji, ale mogą statycznie skompilować ją statycznie do swojej aplikacji z dostarczonego przez Ciebie pliku JAR.

Klasa ContactsContract i jej zagnieżdżone klasy to przykłady klas umów.

Implementowanie uprawnień dostawców 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 Omówienie miejsca na dane i pliki opisaliśmy też zabezpieczenia i uprawnienia związane z różnymi typami pamięci masowej. Oto najważniejsze kwestie:

  • Domyślnie pliki danych przechowywane w pamięci wewnętrznej urządzenia są prywatne dla Twojej aplikacji i dostawcy.
  • Utworzone przez Ciebie bazy danych SQLiteDatabase są prywatne dla Twojej aplikacji i dostawcy.
  • Domyślnie pliki danych zapisywane w pamięci zewnętrznej są publiczne i czytelne dla całego świata. Nie możesz korzystać z usług dostawcy treści, aby ograniczać dostęp do plików w pamięci zewnętrznej, ponieważ inne aplikacje mogą odczytywać i zapisywać pliki w innych aplikacjach za pomocą innych wywołań interfejsu API.
  • Wywołania metody do otwierania lub tworzenia plików bądź baz danych SQLite w pamięci wewnętrznej urządzenia mogą potencjalnie zapewnić wszystkim innym aplikacjom uprawnienia do odczytu i zapisu. Jeśli jako repozytorium dostawcy używasz wewnętrznego pliku lub bazy danych i przyznasz mu dostęp „odczytywany na całym świecie” lub „możliwy do zapisu na całym świecie”, 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 korzystać z uprawnień dostawcy treści do kontrolowania dostępu do swoich danych, możesz przechowywać je w plikach wewnętrznych, bazach danych SQLite lub w chmurze, np. na serwerze zdalnym, i zadbać o to, aby pliki i bazy danych były prywatne dla aplikacji.

Wdrażanie uprawnień

Domyślnie wszystkie aplikacje mogą odczytywać dane od dostawcy lub do niego zapisywać, nawet jeśli dane źródłowe są prywatne, ponieważ domyślnie dostawca nie ma skonfigurowanych uprawnień. Aby to zmienić, ustaw uprawnienia dostawcy w pliku manifestu za pomocą atrybutów lub elementów podrzędnych elementu <provider>. Możesz ustawić uprawnienia dotyczące całego dostawcy, niektórych tabel, niektórych rekordów lub wszystkich trzech.

Uprawnienia dostawcy definiuje się za pomocą co najmniej jednego elementu <permission> w pliku manifestu. Aby utworzyć uprawnienie unikalne dla dostawcy, użyj zakresu w stylu Java dla atrybutu android:name. Na przykład nazwij uprawnienie do odczytu com.example.app.provider.permission.READ_PROVIDER.

Lista poniżej opisuje zakres uprawnień dostawcy: od uprawnień, które mają zastosowanie do całego dostawcy, aż po bardziej szczegółowe. Bardziej szczegółowe uprawnienia mają pierwszeństwo przed uprawnieniami o większym zakresie.

Pojedyncze uprawnienie dostawcy na poziomie dostawcy do odczytu i zapisu
Jedno uprawnienie, które kontroluje dostęp do odczytu i zapisu do całego dostawcy, określone za pomocą atrybutu android:permission elementu <provider>.
Osobne uprawnienia do odczytu i zapisu na poziomie dostawcy
Uprawnienia do odczytu i uprawnienia do 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 identyfikatora URI treści u dostawcy. Każdy identyfikator URI, który chcesz kontrolować, wymaga elementu podrzędnego <path-permission> elementu <provider>. W przypadku każdego podanego identyfikatora URI treści możesz określić uprawnienia do odczytu i zapisu, uprawnienia do odczytu, do zapisu lub wszystkie 3 uprawnienia. Uprawnienia do odczytu i zapisu mają pierwszeństwo przed uprawnieniami do odczytu i zapisu. Ponadto uprawnienia na poziomie ścieżki mają pierwszeństwo przed uprawnieniami na poziomie dostawcy.
Uprawnienia tymczasowe
Poziom uprawnień, który zapewnia tymczasowy dostęp do aplikacji, nawet jeśli nie ma ona zwykle wymaganych uprawnień. Funkcja tymczasowego dostępu zmniejsza liczbę uprawnień, o które aplikacja musi poprosić w swoim manifeście. Po włączeniu uprawnień tymczasowych jedyne aplikacje, które potrzebują trwałych uprawnień dla dostawcy, to te, które mają stały dostęp do wszystkich Twoich danych.

Weź pod uwagę na przykład uprawnienia wymagane, gdy wdrażasz dostawcę i aplikację dostawcy poczty e-mail oraz chcesz pozwolić zewnętrznej aplikacji do przeglądania obrazów na wyświetlanie załączonych zdjęć od tego dostawcy. Aby przyznać przeglądarce obrazów niezbędny dostęp bez wymagania uprawnień, możesz skonfigurować tymczasowe uprawnienia do identyfikatorów URI treści w przypadku zdjęć.

Zaprojektuj aplikację do obsługi poczty e-mail tak, aby gdy użytkownik zechce wyświetlić zdjęcie, wysyła do przeglądarki obrazów intencję zawierającą identyfikator URI treści zdjęcia oraz flagi uprawnień. Przeglądarka obrazów może następnie poprosić dostawcę poczty e-mail o pobranie zdjęcia, nawet jeśli nie ma on zwykłych uprawnień do odczytu u dostawcy.

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

Wartość atrybutu określa, jaka część treści dostawcy jest dostępna. Jeśli atrybut ma wartość "true", system przyznaje tymczasowe uprawnienia całemu dostawcy, zastępując wszelkie inne uprawnienia wymagane przez uprawnienia na poziomie dostawcy lub na poziomie ś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 został przyznany dostęp tymczasowy.

Aby przekazać aplikacji tymczasowy dostęp, intencja musi zawierać flagę FLAG_GRANT_READ_URI_PERMISSION, flagę FLAG_GRANT_WRITE_URI_PERMISSION lub obie te wartości. Są one ustawiane za pomocą metody setFlags().

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

Element <provider>

Podobnie jak w przypadku komponentów Activity i Service, podklasa elementu ContentProvider jest zdefiniowana w pliku manifestu aplikacji przy użyciu elementu <provider>. System Android pobiera z elementu te informacje:

Uprawnienia (android:authorities)
Nazwy symboliczne identyfikujące całego dostawcę w systemie. Szczegółowy opis tego atrybutu znajdziesz w sekcji Projektowanie identyfikatorów URI treści.
Nazwa klasy dostawcy (android:name)
Klasa, która implementuje metodę ContentProvider. Ta klasa jest szczegółowo opisana w sekcji Implementowanie klasy ContentProvider.
Uprawnienia
Atrybuty określające uprawnienia, które muszą mieć inne aplikacje, aby uzyskać dostęp do danych dostawcy:

Uprawnienia i odpowiadające im atrybuty opisano szczegółowo w sekcji Implementowanie uprawnień dostawców treści.

Atrybuty uruchamiania i kontroli
Te atrybuty określają sposób i czas uruchamiania dostawcy przez system Android, 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ść rozpoczęcia działania tego dostawcy w porównaniu z innymi dostawcami 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 uruchamianego przez dostawcę.
  • android:syncable: flaga wskazująca, że dane dostawcy mają być synchronizowane z danymi na serwerze

Wszystkie te atrybuty są opisane w przewodniku po elemencie <provider>.

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

Wszystkie te atrybuty są opisane w przewodniku po elemencie <provider>.

Intencje i dostęp do danych

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

W zależności od działania w intencji aktywność docelowa może też poprosić użytkownika o zmodyfikowanie danych dostawcy. Intencja może też zawierać „dodatkowe” dane wyświetlane w interfejsie przez aktywność docelową. Użytkownik może następnie zmienić te dane, zanim użyjesz ich do modyfikacji danych u dostawcy.

Możesz korzystać z inteligentnego dostępu, aby zwiększyć integralność danych. Dostawca usług może mieć możliwość wstawiania, aktualizowania i usuwania danych zgodnie ze ściśle określoną logiką biznesową. W takiej sytuacji zezwolenie innym aplikacjom bezpośrednio na modyfikowanie danych może skutkować wygenerowaniem nieprawidłowych danych.

Jeśli chcesz, aby deweloperzy korzystali z intencji dostępu, musisz dokładnie go udokumentować. Wyjaśnij, dlaczego intencjonalny dostęp za pomocą interfejsu użytkownika aplikacji jest lepszy niż próba modyfikacji danych za pomocą ich 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 używaniu intencji znajdziesz w artykule Intencje i filtry intencji.

Więcej powiązanych informacji znajdziesz w omówieniu dostawców kalendarza.