Tạo một trình cung cấp nội dung

Trình cung cấp nội dung quản lý quyền truy cập vào kho lưu trữ dữ liệu trung tâm. Bạn sẽ triển khai trình cung cấp dưới dạng một hoặc nhiều lớp trong ứng dụng Android, cùng với các phần tử trong tệp kê khai. Một trong các lớp của bạn triển khai một lớp con của ContentProvider. Đây là giao diện giữa ứng dụng nhà cung cấp của bạn và các ứng dụng khác.

Mặc dù trình cung cấp nội dung được tạo để cung cấp dữ liệu cho các ứng dụng khác, nhưng bạn có thể có các hoạt động trong ứng dụng của mình cho phép người dùng truy vấn và sửa đổi dữ liệu do trình cung cấp của bạn quản lý.

Trang này chứa quy trình cơ bản để xây dựng một trình cung cấp nội dung và danh sách API sẽ sử dụng.

Trước khi bắt đầu tạo bản dựng

Trước khi bắt đầu tạo một nhà cung cấp, hãy cân nhắc những điều sau:

  • Quyết định xem bạn có cần một nhà cung cấp nội dung hay không. Bạn cần xây dựng một trình cung cấp nội dung nếu muốn cung cấp một hoặc nhiều tính năng sau:
    • Bạn muốn cung cấp dữ liệu hoặc tệp phức tạp cho các ứng dụng khác.
    • Bạn muốn cho phép người dùng sao chép dữ liệu phức tạp từ ứng dụng của bạn vào các ứng dụng khác.
    • Bạn muốn cung cấp cụm từ tìm kiếm được đề xuất tuỳ chỉnh bằng khung tìm kiếm.
    • Bạn muốn hiển thị dữ liệu ứng dụng của mình cho các tiện ích.
    • Bạn muốn triển khai các lớp AbstractThreadedSyncAdapter, CursorAdapter hoặc CursorLoader.

    Bạn không cần trình cung cấp dùng cơ sở dữ liệu hoặc các loại bộ nhớ ổn định khác nếu việc sử dụng hoàn toàn trong ứng dụng của bạn và bạn không cần bất kỳ tính năng nào nêu trên. Thay vào đó, bạn có thể sử dụng một trong những hệ thống lưu trữ được mô tả trong bài viết Tổng quan về lưu trữ dữ liệu và tệp.

  • Hãy đọc bài viết Thông tin cơ bản về trình cung cấp nội dung để tìm hiểu thêm về trình cung cấp và cách hoạt động của họ nếu bạn chưa đọc bài viết này.

Tiếp theo, hãy làm theo các bước sau để tạo nhà cung cấp của bạn:

  1. Thiết kế bộ nhớ thô cho dữ liệu của bạn. Trình cung cấp nội dung cung cấp dữ liệu theo hai cách:
    Dữ liệu tệp
    Dữ liệu thường được lưu vào tệp, chẳng hạn như ảnh, âm thanh hoặc video. Lưu trữ tệp trong không gian riêng tư của ứng dụng. Để phản hồi yêu cầu về một tệp từ một ứng dụng khác, ứng dụng cung cấp của bạn có thể cung cấp tên người dùng cho tệp đó.
    Dữ liệu "có cấu trúc"
    Dữ liệu thường đi vào cơ sở dữ liệu, mảng hoặc cấu trúc tương tự. Lưu trữ dữ liệu ở dạng tương thích với bảng hàng và cột. Một hàng đại diện cho một thực thể, chẳng hạn như người hoặc mặt hàng trong kho hàng. Một cột biểu thị một số dữ liệu về thực thể, chẳng hạn như tên hoặc giá của mặt hàng. Một cách phổ biến để lưu trữ loại dữ liệu này là trong cơ sở dữ liệu SQLite, nhưng bạn có thể sử dụng bất kỳ loại bộ nhớ ổn định nào. Để tìm hiểu thêm về các loại bộ nhớ có sẵn trong hệ thống Android, hãy xem phần Thiết kế bộ nhớ dữ liệu.
  2. Xác định cách triển khai cụ thể của lớp ContentProvider và các phương thức bắt buộc. Lớp này là giao diện giữa dữ liệu của bạn và phần còn lại của hệ thống Android. Để biết thêm thông tin về lớp này, hãy xem phần Triển khai lớp ContentProvider.
  3. Xác định chuỗi uỷ quyền, URI nội dung và tên cột của trình cung cấp. Nếu bạn muốn ứng dụng của trình cung cấp xử lý ý định, bạn cũng nên xác định các thao tác theo ý định, dữ liệu bổ sung và cờ. Ngoài ra, hãy xác định các quyền mà bạn yêu cầu cho các ứng dụng muốn truy cập vào dữ liệu của bạn. Hãy cân nhắc định nghĩa tất cả các giá trị này dưới dạng hằng số trong một lớp hợp đồng riêng. Sau đó, bạn có thể hiển thị lớp này cho các nhà phát triển khác. Để biết thêm thông tin về URI nội dung, hãy xem phần Thiết kế URI nội dung. Để biết thêm thông tin về ý định, hãy xem phần Ý định và quyền truy cập dữ liệu.
  4. Thêm các phần không bắt buộc khác, chẳng hạn như dữ liệu mẫu hoặc hoạt động triển khai AbstractThreadedSyncAdapter có thể đồng bộ hoá dữ liệu giữa trình cung cấp và dữ liệu trên đám mây.

Thiết kế bộ nhớ dữ liệu

Trình cung cấp nội dung là giao diện đối với dữ liệu được lưu ở định dạng có cấu trúc. Trước khi bạn tạo giao diện, hãy quyết định cách lưu trữ dữ liệu. Bạn có thể lưu trữ dữ liệu dưới bất kỳ dạng nào bạn muốn, sau đó thiết kế giao diện để đọc và ghi dữ liệu nếu cần.

Dưới đây là một số công nghệ lưu trữ dữ liệu có trên Android:

  • Nếu bạn đang xử lý dữ liệu có cấu trúc, hãy cân nhắc dùng một cơ sở dữ liệu quan hệ như SQLite hoặc một kho dữ liệu khoá-giá trị không có quan hệ như levelDB. Nếu bạn đang làm việc với các loại dữ liệu phi cấu trúc, chẳng hạn như âm thanh, hình ảnh hoặc nội dung nghe nhìn video, hãy cân nhắc việc lưu trữ dữ liệu đó dưới dạng tệp. Bạn có thể kết hợp và so khớp nhiều loại bộ nhớ rồi hiển thị các loại bộ nhớ đó bằng cách sử dụng một trình cung cấp nội dung duy nhất nếu cần.
  • Hệ thống Android có thể tương tác với thư viện Room về dữ liệu cố định. Thư viện này cung cấp quyền truy cập vào API cơ sở dữ liệu SQLite mà các nhà cung cấp của Android dùng để lưu trữ dữ liệu hướng bảng. Để tạo cơ sở dữ liệu bằng thư viện này, hãy tạo thực thể cho một lớp con của RoomDatabase, như mô tả trong phần Lưu dữ liệu trong cơ sở dữ liệu cục bộ bằng Room.

    Bạn không phải sử dụng cơ sở dữ liệu để triển khai kho lưu trữ. Trình cung cấp xuất hiện bên ngoài dưới dạng một tập hợp bảng, tương tự như cơ sở dữ liệu quan hệ, nhưng đây không phải là yêu cầu đối với quá trình triển khai nội bộ của trình cung cấp.

  • Để lưu trữ dữ liệu tệp, Android có nhiều API hướng tệp. Để tìm hiểu thêm về bộ nhớ tệp, hãy đọc bài viết Tổng quan về lưu trữ dữ liệu và tệp. Nếu đang thiết kế một nhà cung cấp cung cấp dữ liệu liên quan đến nội dung nghe nhìn, chẳng hạn như nhạc hoặc video, thì bạn có thể dùng một nhà cung cấp kết hợp dữ liệu bảng và tệp.
  • Trong một số ít trường hợp, bạn có thể hưởng lợi từ việc triển khai nhiều trình cung cấp nội dung cho một ứng dụng. Ví dụ: bạn có thể muốn chia sẻ một số dữ liệu với một tiện ích thông qua một nhà cung cấp nội dung và hiển thị một tập dữ liệu khác để chia sẻ với các ứng dụng khác.
  • Để làm việc với dữ liệu dựa trên mạng, hãy sử dụng các lớp trong java.netandroid.net. Bạn cũng có thể đồng bộ hoá dữ liệu trên mạng với một kho dữ liệu cục bộ, chẳng hạn như cơ sở dữ liệu, rồi cung cấp dữ liệu đó dưới dạng bảng hoặc tệp.

Lưu ý: Nếu thay đổi kho lưu trữ không có khả năng tương thích ngược, bạn cần đánh dấu kho lưu trữ đó bằng số phiên bản mới. Bạn cũng cần tăng số phiên bản cho ứng dụng triển khai trình cung cấp nội dung mới. Sự thay đổi này sẽ ngăn việc hạ cấp hệ thống khiến hệ thống gặp sự cố khi cố gắng cài đặt lại một ứng dụng có trình cung cấp nội dung không tương thích.

Những điều cần cân nhắc khi thiết kế dữ liệu

Dưới đây là một số mẹo để thiết kế cấu trúc dữ liệu của nhà cung cấp:

  • Dữ liệu trong bảng phải luôn có cột "khoá chính" mà trình cung cấp duy trì dưới dạng một giá trị số duy nhất cho mỗi hàng. Bạn có thể sử dụng giá trị này để liên kết hàng đó với các hàng có liên quan trong các bảng khác (sử dụng giá trị đó làm "khoá ngoại"). Mặc dù bạn có thể dùng tên bất kỳ cho cột này, nhưng tốt nhất là bạn nên sử dụng BaseColumns._ID. Lý do là việc liên kết kết quả truy vấn trình cung cấp với ListView yêu cầu một trong các cột được truy xuất phải có tên là _ID.
  • Nếu bạn muốn cung cấp hình ảnh bitmap hoặc các phần dữ liệu rất lớn khác theo hướng tệp, hãy lưu trữ dữ liệu trong một tệp rồi cung cấp dữ liệu đó gián tiếp thay vì lưu trữ trực tiếp trong bảng. Nếu làm như vậy, bạn cần cho người dùng của nhà cung cấp biết rằng họ cần sử dụng phương thức tệp ContentResolver để truy cập vào dữ liệu.
  • Sử dụng loại dữ liệu đối tượng lớn nhị phân (BLOB) để lưu trữ dữ liệu có kích thước khác nhau hoặc có cấu trúc thay đổi. Ví dụ: bạn có thể dùng cột BLOB để lưu trữ vùng đệm giao thức hoặc cấu trúc JSON.

    Bạn cũng có thể sử dụng BLOB để triển khai bảng độc lập với giản đồ. Trong loại bảng này, bạn sẽ xác định một cột khoá chính, một cột loại MIME và một hoặc nhiều cột chung là BLOB. Ý nghĩa của dữ liệu trong các cột BLOB được biểu thị bằng giá trị trong cột loại MIME. Điều này cho phép bạn lưu trữ nhiều loại hàng trong cùng một bảng. Bảng "dữ liệu" của Trình cung cấp danh bạ ContactsContract.Data là một ví dụ về bảng độc lập với giản đồ.

Thiết kế URI nội dung

URI nội dung là một URI xác định dữ liệu trong một trình cung cấp. URI nội dung bao gồm tên tượng trưng của toàn bộ trình cung cấp (quyền hạn của trình cung cấp đó) và tên trỏ đến một bảng hoặc tệp (một đường dẫn). Phần mã nhận dạng không bắt buộc trỏ đến một hàng riêng lẻ trong bảng. Mọi phương thức truy cập dữ liệu của ContentProvider đều có một URI nội dung làm đối số. Điều này cho phép bạn xác định bảng, hàng hoặc tệp cần truy cập.

Để biết thông tin về URI nội dung, hãy xem phần Thông tin cơ bản về trình cung cấp nội dung.

Thiết kế một cơ quan quản lý

Nhà cung cấp thường có một đơn vị quản lý duy nhất, đóng vai trò là tên nội bộ trong Android. Để tránh xung đột với các nhà cung cấp khác, hãy lấy quyền sở hữu miền Internet (ngược lại) làm cơ sở cho thẩm quyền của nhà cung cấp đó. Vì đề xuất này cũng đúng với tên gói Android, bạn có thể xác định thẩm quyền của trình cung cấp là một phần mở rộng của tên gói chứa trình cung cấp đó.

Ví dụ: nếu tên gói Android của bạn là com.example.<appname>, hãy cấp cho nhà cung cấp quyền com.example.<appname>.provider.

Thiết kế cấu trúc đường dẫn

Nhà phát triển thường tạo URI nội dung từ đơn vị có thẩm quyền bằng cách thêm đường dẫn trỏ đến từng bảng. Ví dụ: nếu có hai bảng là table1table2, thì bạn có thể kết hợp các bảng này với thẩm quyền trong ví dụ trước để tạo các URI nội dung com.example.<appname>.provider/table1com.example.<appname>.provider/table2. Đường dẫn không bị giới hạn ở một phân đoạn duy nhất và không nhất thiết phải có bảng cho từng cấp của đường dẫn.

Xử lý mã nhận dạng URI nội dung

Theo quy ước, nhà cung cấp sẽ cấp quyền truy cập vào một hàng trong bảng bằng cách chấp nhận một URI nội dung có giá trị mã nhận dạng cho hàng ở cuối URI. Ngoài ra, theo quy ước, trình cung cấp sẽ so khớp giá trị mã nhận dạng với cột _ID của bảng và thực hiện quyền truy cập theo yêu cầu đối với hàng khớp.

Quy ước này tạo điều kiện cho một mẫu thiết kế chung cho các ứng dụng truy cập vào trình cung cấp. Ứng dụng thực hiện một truy vấn dựa trên trình cung cấp và hiển thị Cursor kết quả trong ListView bằng cách sử dụng CursorAdapter. Định nghĩa của CursorAdapter yêu cầu một trong các cột trong Cursor phải là _ID

Sau đó, người dùng chọn một trong các hàng hiển thị trên giao diện người dùng để xem hoặc sửa đổi dữ liệu. Ứng dụng nhận hàng tương ứng từ Cursor sao lưu ListView, nhận giá trị _ID cho hàng này, thêm giá trị đó vào URI nội dung và gửi yêu cầu truy cập đến trình cung cấp. Sau đó, ứng dụng nhà cung cấp có thể thực hiện truy vấn hoặc sửa đổi dựa trên hàng chính xác mà người dùng đã chọn.

Mẫu URI nội dung

Để giúp bạn chọn hành động cần thực hiện cho URI nội dung sắp tới, API nhà cung cấp bao gồm lớp tiện lợi UriMatcher. Lớp này ánh xạ các mẫu URI nội dung với giá trị số nguyên. Bạn có thể sử dụng các giá trị số nguyên trong câu lệnh switch để chọn thao tác mong muốn cho URI nội dung hoặc URI khớp với một mẫu cụ thể.

Mẫu URI nội dung khớp với các URI nội dung bằng cách dùng ký tự đại diện:

  • * khớp với một chuỗi gồm mọi ký tự hợp lệ có độ dài bất kỳ.
  • # khớp với một chuỗi các ký tự số có độ dài bất kỳ.

Hãy xem ví dụ về cách thiết kế và lập trình cho cách xử lý URI nội dung, hãy cân nhắc sử dụng trình cung cấp có thẩm quyền com.example.app.provider nhận dạng các URI nội dung sau trỏ đến bảng:

  • content://com.example.app.provider/table1: một bảng có tên là table1.
  • content://com.example.app.provider/table2/dataset1: một bảng có tên là dataset1.
  • content://com.example.app.provider/table2/dataset2: một bảng có tên là dataset2.
  • content://com.example.app.provider/table3: một bảng có tên là table3.

Trình cung cấp cũng nhận ra những URI nội dung này nếu chúng có một mã hàng được thêm vào, chẳng hạn như content://com.example.app.provider/table3/1 cho hàng do 1 xác định trong table3.

Có thể có các mẫu URI nội dung sau đây:

content://com.example.app.provider/*
So khớp với mọi URI nội dung trong trình cung cấp.
content://com.example.app.provider/table2/*
So khớp một URI nội dung cho các bảng dataset1dataset2, nhưng không khớp với các URI nội dung cho table1 hoặc table3.
content://com.example.app.provider/table3/#
So khớp một URI nội dung cho các hàng đơn lẻ trong table3, chẳng hạn như content://com.example.app.provider/table3/6 cho hàng do 6 xác định.

Đoạn mã sau đây minh hoạ cách hoạt động của các phương thức trong UriMatcher. Mã này xử lý URI cho toàn bộ bảng khác với URI cho một hàng bằng cách sử dụng mẫu URI nội dung content://<authority>/<path> cho bảng và content://<authority>/<path>/<id> cho các hàng đơn lẻ.

Phương thức addURI() liên kết một đơn vị quản lý và đường dẫn đến một giá trị số nguyên. Phương thức match() trả về giá trị số nguyên cho một URI. Câu lệnh switch lựa chọn giữa việc truy vấn toàn bộ bảng và truy vấn một bản ghi.

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
    }

Một lớp khác là ContentUris cung cấp các phương thức tiện lợi để làm việc với phần id của URI nội dung. Các lớp UriUri.Builder bao gồm các phương thức thuận tiện để phân tích cú pháp các đối tượng Uri hiện có và tạo các đối tượng mới.

Triển khai lớp ContentProvider

Thực thể ContentProvider quản lý quyền truy cập vào một tập dữ liệu có cấu trúc bằng cách xử lý yêu cầu của các ứng dụng khác. Cuối cùng, tất cả các hình thức truy cập sẽ gọi ContentResolver, sau đó gọi một phương thức cụ thể của ContentProvider để lấy quyền truy cập.

Phương thức bắt buộc

Lớp trừu tượng ContentProvider xác định 6 phương thức trừu tượng mà bạn triển khai trong lớp con cụ thể. Tất cả các phương thức này, ngoại trừ onCreate(), được gọi bởi một ứng dụng khách đang cố truy cập vào trình cung cấp nội dung của bạn.

query()
Truy xuất dữ liệu từ nhà cung cấp của bạn. Sử dụng các đối số để chọn bảng để truy vấn, các hàng và cột cần trả về, cũng như thứ tự sắp xếp kết quả. Trả về dữ liệu dưới dạng đối tượng Cursor.
insert()
Chèn một hàng mới vào ứng dụng nhà cung cấp của bạn. Sử dụng các đối số để chọn bảng đích đến và lấy các giá trị cột để sử dụng. Trả về một URI nội dung cho hàng mới được chèn.
update()
Cập nhật các hàng hiện có trong trình cung cấp của bạn. Sử dụng các đối số để chọn bảng và các hàng cần cập nhật cũng như nhận các giá trị đã cập nhật của cột. Trả về số lượng hàng đã cập nhật.
delete()
Xoá hàng khỏi trình cung cấp của bạn. Dùng các đối số để chọn bảng và các hàng cần xoá. Trả về số lượng hàng đã xoá.
getType()
Trả về loại MIME tương ứng với một URI nội dung. Phương thức này được mô tả chi tiết hơn trong phần Triển khai loại MIME của nhà cung cấp nội dung.
onCreate()
Khởi chạy trình cung cấp. Hệ thống Android gọi phương thức này ngay sau khi tạo ứng dụng cung cấp. Trình cung cấp của bạn không được tạo cho đến khi đối tượng ContentResolver cố gắng truy cập vào trình cung cấp đó.

Các phương thức này có cùng chữ ký với các phương thức ContentResolver giống hệt nhau.

Khi triển khai các phương thức này, bạn cần tính đến những yếu tố sau:

  • Nhiều luồng có thể gọi tất cả các phương thức này cùng một lúc, ngoại trừ onCreate(). Vì vậy, các phương thức này cần phải an toàn cho luồng. Để tìm hiểu thêm về nhiều luồng, hãy xem bài viết Tổng quan về quy trình và luồng.
  • Tránh thực hiện các thao tác dài trong onCreate(). Hoãn các tác vụ khởi chạy cho đến khi thực sự cần thiết. Phần về cách triển khai phương thức onCreate() sẽ thảo luận chi tiết hơn về điều này.
  • Mặc dù bạn phải triển khai các phương thức này, nhưng mã của bạn không phải làm gì ngoài việc trả về loại dữ liệu dự kiến. Ví dụ: bạn có thể ngăn các ứng dụng khác chèn dữ liệu vào một số bảng bằng cách bỏ qua lệnh gọi đến insert() và trả về 0.

Triển khai phương thức query()

Phương thức ContentProvider.query() phải trả về một đối tượng Cursor hoặc nếu không thành công, hãy gửi một Exception. Nếu đang sử dụng cơ sở dữ liệu SQLite làm bộ nhớ dữ liệu, bạn có thể trả về Cursor bằng một trong các phương thức query() của lớp SQLiteDatabase.

Nếu truy vấn không khớp với hàng nào, hãy trả về một phiên bản Cursor có phương thức getCount() trả về 0. Chỉ trả về null nếu xảy ra lỗi nội bộ trong quá trình truy vấn.

Nếu bạn không sử dụng cơ sở dữ liệu SQLite làm bộ nhớ dữ liệu, hãy sử dụng một trong các lớp con cụ thể của Cursor. Ví dụ: lớp MatrixCursor triển khai một con trỏ, trong đó mỗi hàng là một mảng gồm các thực thể Object. Với lớp này, hãy sử dụng addRow() để thêm một hàng mới.

Hệ thống Android phải có khả năng giao tiếp Exception qua ranh giới của quy trình. Android có thể thực hiện việc này cho các ngoại lệ sau đây (hữu ích trong việc xử lý lỗi truy vấn):

Triển khai phương thức insert()

Phương thức insert() sẽ thêm một hàng mới vào bảng thích hợp bằng cách sử dụng các giá trị trong đối số ContentValues. Nếu tên cột không có trong đối số ContentValues, thì bạn nên cung cấp giá trị mặc định cho đối số đó trong mã nhà cung cấp hoặc trong giản đồ cơ sở dữ liệu.

Phương thức này trả về URI nội dung của hàng mới. Để tạo trường này, hãy thêm khoá chính của hàng mới (thường là giá trị _ID) vào URI nội dung của bảng bằng withAppendedId().

Triển khai phương thức delete()

Phương thức delete() không phải xoá các hàng khỏi bộ nhớ dữ liệu của bạn. Nếu bạn đang sử dụng bộ điều hợp đồng bộ hoá với nhà cung cấp, hãy cân nhắc đánh dấu một hàng đã xoá bằng cờ "xoá" thay vì xoá hoàn toàn hàng đó. Bộ điều hợp đồng bộ hoá có thể kiểm tra các hàng đã bị xoá và loại bỏ các hàng đó khỏi máy chủ trước khi xoá các hàng đó khỏi trình cung cấp.

Triển khai phương thức update()

Phương thức update() lấy cùng một đối số ContentValuesinsert() sử dụng, cũng như các đối số selectionselectionArgsdelete()ContentProvider.query() sử dụng. Điều này có thể cho phép bạn sử dụng lại mã giữa các phương thức này.

Triển khai phương thức onCreate()

Hệ thống Android gọi onCreate() khi khởi động ứng dụng nhà cung cấp. Chỉ thực hiện các tác vụ khởi chạy chạy nhanh trong phương thức này, đồng thời trì hoãn việc tạo cơ sở dữ liệu và tải dữ liệu cho đến khi trình cung cấp thực sự nhận được yêu cầu về dữ liệu. Nếu thực hiện các tác vụ dài trong onCreate(), bạn sẽ làm chậm quá trình khởi động của trình cung cấp. Đổi lại, điều này làm chậm phản hồi của trình cung cấp tới các ứng dụng khác.

Hai đoạn mã sau đây minh hoạ sự tương tác giữa ContentProvider.onCreate() Room.databaseBuilder(). Đoạn mã đầu tiên cho thấy cách triển khai ContentProvider.onCreate(), trong đó đối tượng cơ sở dữ liệu được tạo và xử lý các đối tượng truy cập dữ liệu được tạo:

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.
    }
}

Triển khai các loại MIME ContentProvider

Lớp ContentProvider có hai phương thức để trả về loại MIME:

getType()
Một trong những phương thức bắt buộc mà bạn triển khai cho mọi trình cung cấp.
getStreamTypes()
Bạn nên triển khai phương thức này nếu nhà cung cấp của bạn cung cấp tệp.

Loại MIME cho bảng

Phương thức getType() trả về một String ở định dạng MIME mô tả loại dữ liệu mà đối số URI nội dung trả về. Đối số Uri có thể là một mẫu thay vì một URI cụ thể. Trong trường hợp này, hãy trả về loại dữ liệu liên kết với các URI nội dung khớp với mẫu đó.

Đối với các loại dữ liệu phổ biến như văn bản, HTML hoặc JPEG, getType() sẽ trả về loại MIME tiêu chuẩn cho dữ liệu đó. Danh sách đầy đủ các loại tiêu chuẩn này có trên trang web Loại nội dung nghe nhìn IANA MIME.

Đối với các URI nội dung trỏ đến một hàng hoặc các hàng dữ liệu trong bảng, getType() sẽ trả về một loại MIME ở định dạng MIME dành riêng cho nhà cung cấp của Android:

  • Phần kiểu: vnd
  • Phần loại phụ:
    • Nếu mẫu URI dành cho một hàng duy nhất: android.cursor.item/
    • Nếu mẫu URI dành cho nhiều hàng: android.cursor.dir/
  • Phần dành riêng cho nhà cung cấp: vnd.<name>.<type>

    Bạn cung cấp <name><type>. Giá trị <name> là duy nhất trên toàn hệ thống và giá trị <type> là duy nhất cho mẫu URI tương ứng. Lựa chọn phù hợp cho <name> là tên công ty hoặc một phần trong tên gói Android của ứng dụng. Một lựa chọn phù hợp cho <type> là một chuỗi xác định bảng liên kết với URI.

Ví dụ: nếu quyền của trình cung cấp là com.example.app.provider và hiển thị một bảng có tên table1, thì loại MIME cho nhiều hàng trong table1 sẽ là:

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

Đối với một hàng table1, loại MIME là:

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

Loại MIME cho tệp

Nếu nhà cung cấp của bạn cung cấp tệp, hãy triển khai getStreamTypes(). Phương thức này sẽ trả về một mảng String gồm các loại MIME cho các tệp mà nhà cung cấp của bạn có thể trả về cho một URI nội dung nhất định. Hãy lọc các loại MIME mà bạn cung cấp theo đối số bộ lọc loại MIME để bạn chỉ trả về các loại MIME mà ứng dụng muốn xử lý.

Ví dụ: hãy xem xét một nhà cung cấp hình ảnh dưới dạng tệp JPG, PNG và GIF. Nếu ứng dụng gọi ContentResolver.getStreamTypes() bằng chuỗi bộ lọc image/*, đối với nội dung là "hình ảnh", thì phương thức ContentProvider.getStreamTypes() sẽ trả về mảng:

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

Nếu chỉ quan tâm đến tệp JPG, ứng dụng có thể gọi ContentResolver.getStreamTypes() bằng chuỗi bộ lọc *\/jpeggetStreamTypes() trả về:

{"image/jpeg"}

Nếu nhà cung cấp của bạn không cung cấp bất kỳ loại MIME nào được yêu cầu trong chuỗi bộ lọc, thì getStreamTypes() sẽ trả về null.

Triển khai một lớp hợp đồng

Lớp hợp đồng là một lớp public final chứa các định nghĩa không đổi cho URI, tên cột, loại MIME và siêu dữ liệu khác liên quan đến nhà cung cấp. Lớp này thiết lập một hợp đồng giữa trình cung cấp và các ứng dụng khác bằng cách đảm bảo rằng ứng dụng có thể được truy cập chính xác ngay cả khi có những thay đổi đối với giá trị thực tế của URI, tên cột, v.v.

Lớp hợp đồng cũng giúp ích cho nhà phát triển vì lớp này thường có tên dễ ghi nhớ cho các hằng số, nhờ đó, nhà phát triển ít có khả năng sử dụng giá trị không chính xác cho tên cột hoặc URI. Vì là một lớp nên lớp này có thể chứa tài liệu Javadoc. Các môi trường phát triển tích hợp như Android Studio có thể tự động hoàn thành tên hằng số từ lớp hợp đồng và hiển thị Javadoc cho các hằng số đó.

Các nhà phát triển không thể truy cập vào tệp lớp của lớp hợp đồng qua ứng dụng của bạn, nhưng họ có thể biên dịch tĩnh tệp đó vào ứng dụng từ tệp JAR mà bạn cung cấp.

Lớp ContactsContract và các lớp lồng ghép trong đó là ví dụ về lớp hợp đồng.

Triển khai quyền của trình cung cấp nội dung

Quyền và quyền truy cập vào mọi khía cạnh của hệ thống Android được mô tả chi tiết trong Mẹo bảo mật. Tổng quan về lưu trữ dữ liệu và tệp cũng mô tả tính bảo mật và các quyền có hiệu lực đối với nhiều loại hình lưu trữ. Tóm lại, những điểm quan trọng sau đây:

  • Theo mặc định, các tệp dữ liệu lưu trữ trên bộ nhớ trong của thiết bị chỉ dành riêng cho ứng dụng và nhà cung cấp của bạn.
  • Các cơ sở dữ liệu SQLiteDatabase mà bạn tạo sẽ ở chế độ riêng tư đối với ứng dụng và trình cung cấp của bạn.
  • Theo mặc định, các tệp dữ liệu mà bạn lưu vào bộ nhớ ngoài sẽ ở chế độ công khaicó thể đọc được trên toàn thế giới. Bạn không thể sử dụng trình cung cấp nội dung để hạn chế quyền truy cập vào các tệp trong bộ nhớ ngoài, vì các ứng dụng khác có thể dùng lệnh gọi API khác để đọc và ghi các tệp đó.
  • Phương thức này sẽ gọi để mở hoặc tạo tệp hoặc cơ sở dữ liệu SQLite trên bộ nhớ trong của thiết bị có thể cấp cả quyền đọc và ghi đối với tất cả ứng dụng khác. Nếu bạn sử dụng một tệp hoặc cơ sở dữ liệu nội bộ làm kho lưu trữ của nhà cung cấp và cấp cho kho lưu trữ đó quyền truy cập "có thể đọc được trên thế giới" hoặc "có thể ghi trên thế giới", thì những quyền bạn thiết lập cho trình cung cấp của bạn trong tệp kê khai sẽ không bảo vệ dữ liệu của bạn. Quyền truy cập mặc định cho các tệp và cơ sở dữ liệu trong bộ nhớ trong là "riêng tư"; đừng thay đổi quyền này đối với kho lưu trữ của nhà cung cấp.

Nếu bạn muốn sử dụng quyền của trình cung cấp nội dung để kiểm soát quyền truy cập vào dữ liệu của mình, hãy lưu trữ dữ liệu trong các tệp nội bộ, cơ sở dữ liệu SQLite hoặc đám mây (chẳng hạn như trên máy chủ từ xa) và đảm bảo riêng tư các tệp và cơ sở dữ liệu cho ứng dụng của bạn.

Triển khai quyền

Theo mặc định, tất cả các ứng dụng đều có thể đọc hoặc ghi vào ứng dụng nhà cung cấp của bạn, ngay cả khi dữ liệu cơ bản ở chế độ riêng tư, vì theo mặc định, ứng dụng chưa thiết lập các quyền. Để thay đổi tình trạng này, hãy thiết lập quyền cho trình cung cấp của bạn trong tệp kê khai, sử dụng các thuộc tính hoặc phần tử con của phần tử <provider>. Bạn có thể đặt các quyền áp dụng cho toàn bộ trình cung cấp, một số bảng, một số bản ghi hoặc cả ba bản ghi.

Bạn xác định các quyền cho trình cung cấp bằng một hoặc nhiều phần tử <permission> trong tệp kê khai. Để tạo quyền duy nhất cho trình cung cấp của bạn, hãy sử dụng tính năng xác định phạm vi kiểu Java cho thuộc tính android:name. Ví dụ: đặt tên cho quyền đọc com.example.app.provider.permission.READ_PROVIDER.

Danh sách sau đây mô tả phạm vi của quyền của nhà cung cấp, bắt đầu từ những quyền áp dụng cho toàn bộ nhà cung cấp rồi sau đó đi vào chi tiết hơn. Các quyền chi tiết hơn sẽ được ưu tiên so với các quyền có phạm vi lớn hơn.

Quyền đọc-ghi ở cấp nhà cung cấp
Một quyền kiểm soát cả quyền đọc và ghi đối với toàn bộ trình cung cấp, được chỉ định bằng thuộc tính android:permission của phần tử <provider>.
Phân tách quyền đọc và ghi ở cấp nhà cung cấp
Quyền đọc và quyền ghi cho toàn bộ trình cung cấp. Bạn chỉ định các đối tượng này bằng các thuộc tính android:readPermission android:writePermission của phần tử <provider>. Quyền này được ưu tiên hơn quyền mà android:permission yêu cầu.
Quyền ở cấp đường dẫn
Quyền đọc, ghi hoặc đọc/ghi đối với URI nội dung trong nhà cung cấp của bạn. Bạn chỉ định từng URI mà bạn muốn kiểm soát bằng phần tử con <path-permission> của phần tử <provider>. Đối với mỗi URI nội dung mà bạn chỉ định, bạn có thể chỉ định một quyền đọc/ghi, một quyền đọc, một quyền ghi hoặc cả ba. Quyền đọc và quyền ghi được ưu tiên hơn quyền đọc/ghi. Ngoài ra, quyền ở cấp đường dẫn sẽ được ưu tiên hơn quyền ở cấp nhà cung cấp.
Quyền tạm thời
Cấp độ quyền cấp quyền truy cập tạm thời vào một ứng dụng, ngay cả khi ứng dụng đó không có các quyền thường được yêu cầu. Tính năng truy cập tạm thời giúp giảm số lượng quyền mà một ứng dụng phải yêu cầu trong tệp kê khai. Khi bạn bật quyền tạm thời, chỉ những ứng dụng cần có quyền vĩnh viễn cho ứng dụng của bạn là những ứng dụng liên tục truy cập vào tất cả dữ liệu của bạn.

Ví dụ: hãy xem xét các quyền bạn cần nếu bạn đang triển khai một ứng dụng và nhà cung cấp dịch vụ email, đồng thời bạn muốn cho phép một ứng dụng xem hình ảnh bên ngoài hiển thị tệp đính kèm ảnh từ nhà cung cấp của bạn. Để cấp cho người xem hình ảnh quyền truy cập cần thiết mà không cần quyền, bạn có thể thiết lập quyền tạm thời cho URI nội dung dành cho ảnh.

Hãy thiết kế ứng dụng email của bạn để khi người dùng muốn hiển thị ảnh, ứng dụng sẽ gửi ý định chứa URI nội dung của ảnh và cờ quyền cho trình xem hình ảnh. Sau đó, trình xem hình ảnh có thể truy vấn nhà cung cấp dịch vụ email của bạn để truy xuất ảnh, mặc dù người xem không có quyền đọc thông thường đối với nhà cung cấp đó.

Để bật quyền tạm thời, hãy đặt thuộc tính android:grantUriPermissions của phần tử <provider> hoặc thêm một hoặc nhiều phần tử con <grant-uri-permission> vào phần tử <provider>. Gọi Context.revokeUriPermission() mỗi khi bạn ngừng hỗ trợ URI nội dung liên kết với quyền tạm thời của nhà cung cấp.

Giá trị của thuộc tính này xác định mức độ truy cập của nhà cung cấp. Nếu bạn đặt thuộc tính này thành "true", thì hệ thống sẽ cấp quyền tạm thời cho toàn bộ trình cung cấp của bạn, ghi đè mọi quyền khác mà quyền cấp nhà cung cấp hoặc cấp đường dẫn yêu cầu.

Nếu bạn đặt cờ này thành "false", hãy thêm các phần tử con <grant-uri-permission> vào phần tử <provider>. Mỗi phần tử con chỉ định URI nội dung hoặc URI được cấp quyền truy cập tạm thời.

Để uỷ quyền quyền truy cập tạm thời vào một ứng dụng, một ý định phải chứa cờ FLAG_GRANT_READ_URI_PERMISSION, cờ FLAG_GRANT_WRITE_URI_PERMISSION hoặc cả hai. Các giá trị này được đặt bằng phương thức setFlags().

Nếu không có thuộc tính android:grantUriPermissions, thuộc tính này được giả định là "false".

Phần tử <provider>

Giống như các thành phần ActivityService, một lớp con của ContentProvider được xác định trong tệp kê khai cho ứng dụng của lớp đó bằng cách dùng phần tử <provider>. Hệ thống Android sẽ lấy thông tin sau đây qua phần tử này:

Đơn vị quản lý (android:authorities)
Tên tượng trưng xác định toàn bộ trình cung cấp trong hệ thống. Thuộc tính này được mô tả chi tiết hơn trong phần Thiết kế URI nội dung.
Tên lớp của nhà cung cấp (android:name)
Lớp triển khai ContentProvider. Lớp này được mô tả chi tiết hơn trong phần Triển khai lớp ContentProvider.
Quyền
Các thuộc tính chỉ định quyền mà các ứng dụng khác cần phải có để truy cập vào dữ liệu của trình cung cấp:

Các quyền và thuộc tính tương ứng được mô tả chi tiết hơn trong phần Triển khai quyền của trình cung cấp nội dung.

Thuộc tính khởi động và kiểm soát
Những thuộc tính này xác định cách thức và thời điểm hệ thống Android khởi động trình cung cấp, đặc điểm xử lý của trình cung cấp và các chế độ cài đặt thời gian chạy khác:
  • android:enabled: gắn cờ cho phép hệ thống khởi động ứng dụng nhà cung cấp
  • android:exported: gắn cờ cho phép các ứng dụng khác sử dụng trình cung cấp này
  • android:initOrder: thứ tự bắt đầu trình cung cấp này, so với các trình cung cấp khác trong cùng một quy trình
  • android:multiProcess: gắn cờ cho phép hệ thống khởi động ứng dụng nhà cung cấp trong cùng một quy trình với ứng dụng gọi
  • android:process: tên của quy trình mà trình cung cấp chạy
  • android:syncable: cờ cho biết rằng dữ liệu của nhà cung cấp sẽ được đồng bộ hoá với dữ liệu trên máy chủ

Các thuộc tính này được ghi lại đầy đủ trong hướng dẫn cho phần tử <provider>.

Thuộc tính cung cấp thông tin
Biểu tượng và nhãn không bắt buộc cho trình cung cấp:
  • android:icon: một tài nguyên có thể vẽ chứa biểu tượng cho ứng dụng nhà cung cấp. Biểu tượng này xuất hiện bên cạnh nhãn của nhà cung cấp trong danh sách ứng dụng ở phần Cài đặt > Ứng dụng > Tất cả.
  • android:label: nhãn thông tin mô tả nhà cung cấp, dữ liệu của họ hoặc cả hai. Nhãn này xuất hiện trong danh sách ứng dụng ở phần Cài đặt > Ứng dụng > Tất cả.

Các thuộc tính này được ghi lại đầy đủ trong hướng dẫn cho phần tử <provider>.

Lưu ý: Nếu bạn đang nhắm đến Android 11 trở lên, hãy xem tài liệu về chế độ hiển thị gói để biết thêm các nhu cầu về cấu hình.

Ý định và quyền truy cập dữ liệu

Các ứng dụng có thể truy cập gián tiếp vào trình cung cấp nội dung bằng Intent. Ứng dụng không gọi phương thức nào của ContentResolver hoặc ContentProvider. Thay vào đó, lệnh này sẽ gửi một ý định bắt đầu một hoạt động, thường là một phần trong ứng dụng riêng của trình cung cấp. Hoạt động đích chịu trách nhiệm truy xuất và hiển thị dữ liệu trong giao diện người dùng.

Tuỳ thuộc vào hành động trong ý định, hoạt động đích cũng có thể nhắc người dùng sửa đổi dữ liệu của trình cung cấp. Một ý định cũng có thể chứa dữ liệu "bổ sung" mà hoạt động đích hiển thị trong giao diện người dùng. Sau đó, người dùng có thể thay đổi dữ liệu này trước khi dùng để sửa đổi dữ liệu trong trình cung cấp.

Bạn có thể sử dụng quyền truy cập ý định để giúp đảm bảo tính toàn vẹn của dữ liệu. Nhà cung cấp của bạn có thể phụ thuộc vào việc chèn, cập nhật và xoá dữ liệu theo logic nghiệp vụ được xác định nghiêm ngặt. Trong trường hợp này, việc cho phép các ứng dụng khác sửa đổi trực tiếp dữ liệu của bạn có thể dẫn đến dữ liệu không hợp lệ.

Nếu bạn muốn nhà phát triển sử dụng quyền truy cập theo ý định, hãy nhớ ghi lại kỹ lưỡng. Giải thích lý do tại sao quyền truy cập ý định bằng giao diện người dùng của ứng dụng lại tốt hơn việc cố gắng sửa đổi dữ liệu bằng mã của ứng dụng.

Việc xử lý một ý định đến muốn sửa đổi dữ liệu của trình cung cấp không khác với việc xử lý các ý định khác. Bạn có thể tìm hiểu thêm về cách sử dụng ý định trong bài viết Ý định và bộ lọc ý định.

Để biết thêm thông tin liên quan, hãy tham khảo bài viết Tổng quan về nhà cung cấp Lịch.