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 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 lớp con của ContentProvider, là giao diện giữa trì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 là việc 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 cho phép người dùng truy vấn và sửa đổi dữ liệu do ứng dụng 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 các API cần sử dụng.

Trước khi bạn bắt đầu xây dựng

Trước khi bắt đầu xây dựng 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 trì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 trong ứng dụng của mình sang 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 một trình cung cấp để sử 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 nằm trong ứng dụng của bạn và bạn không cần tính năng nào nêu trên. Thay vào đó, bạn có thể sử dụng một trong các hệ thống lưu trữ như mô tả trong Tổng quan về lưu trữ dữ liệu và tệp.

  • Nếu bạn chưa thực hiện việc này, hãy đọc bài viết Kiến thức 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 thức hoạt động của họ.

Tiếp theo, hãy làm theo các bước sau để tạo ứng dụng nhà cung cấp:

  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 chuyển vào các tệp, chẳng hạn như ảnh, âm thanh hoặc video. Lưu trữ các tệp trong không gian riêng tư của ứng dụng. Để phản hồi yêu cầu về tệp từ một ứng dụng khác, nhà cung cấp của bạn có thể đưa ra một 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. Hàng đại diện cho một thực thể, chẳng hạn như một người hoặc một mặt hàng trong khoảng không quảng cáo. Một cột đại diện cho một số dữ liệu về thực thể, chẳng hạn như tên người 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ể 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ó trong hệ thống Android, hãy xem phần Thiết kế bộ nhớ dữ liệu.
  2. Xác định phương thức triển khai cụ thể của lớp ContentProvider và các phương thức bắt buộc của lớp đó. 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ý các ý định, hãy xác định các thao tác theo ý định, dữ liệu bổ sung và cờ. Đồng thời, hãy xác định các quyền mà bạn yêu cầu cho những ứng dụng muốn truy cập vào dữ liệu của bạn. Hãy cân nhắc việc xác định 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 biệt. Sau đó, bạn có thể cung cấp 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 phương thức 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 cho dữ liệu được lưu ở định dạng có cấu trúc. Trước khi 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 khi 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 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 dữ liệu không có cấu trúc như nội dung nghe nhìn dạng âm thanh, hình ảnh hoặc 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ớ cũng như hiển thị chúng bằng một trình cung cấp nội dung 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 theo 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 cần 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 bắt buộc đối với hoạt động 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ề lưu trữ 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ể tạo một trì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 bằng cách sử dụng một trì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 dựa 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), sau đó cung cấp dữ liệu đó dưới dạng bảng hoặc tệp.

Lưu ý: Nếu thực hiện thay đổi đối vớ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. Thay đổi này sẽ ngăn quá trình 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 về 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ó một cột "khoá chính" mà trình cung cấp duy trì dưới dạng 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ị này làm "khoá nước 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 dùng BaseColumns._ID, vì việc liên kết kết quả của truy vấn trình cung cấp với ListView yêu cầu một trong các cột đã truy xuất 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 hướng tệp rất lớn khác, hãy lưu trữ dữ liệu trong một tệp, sau đó cung cấp dữ liệu gián tiếp thay vì lưu trữ trực tiếp trong bảng. Nếu thực hiện việc nà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 dữ liệu.
  • Sử dụng loại dữ liệu nhị phân đối tượng lớn (BLOB) để lưu trữ dữ liệu thay đổi kích thước hoặc có cấu trúc thay đổi. Ví dụ: bạn có thể sử 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 không phụ thuộc vào giản đồ. Trong loại bảng này, bạn 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 của một 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 (cơ quan của trình cung cấp đó) và tên trỏ đến một bảng hoặc tệp (đườ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 Kiến thức cơ bản về trình cung cấp nội dung.

Thiết kế đơn vị quản lý

Mỗi nhà cung cấp thường có một thẩm quyền duy nhất, đóng vai trò là tên nội bộ trên Android. Để tránh xung đột với các nhà cung cấp khác, hãy dùng 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 áp dụng cho tên gói Android, nên bạn có thể xác định thẩm quyền của trình cung cấp dưới dạng phần mở rộng của tên gói chứa 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

Các nhà phát triển thường tạo URI nội dung từ đơn vị quản lý bằng cách thêm các đường dẫn trỏ đến từng bảng riêng lẻ. Ví dụ: nếu có hai bảng là table1table2, 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 ra 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 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, trình cung cấp cung cấp quyền truy cập vào một hàng duy nhất 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, các 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 được yêu cầu đối với hàng khớp.

Quy ước này hỗ trợ một mẫu thiết kế phổ biến cho các ứng dụng truy cập vào một nhà cung cấp. Ứng dụng thực hiện 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 về 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 sẽ nhận hàng tương ứng từ Cursor sao lưu ListView, lấy giá trị _ID cho hàng này, thêm giá trị đó vào URI nội dung rồi gửi yêu cầu truy cập đến trình cung cấp. Sau đó, trình cung cấp này có thể thực hiện truy vấn hoặc sửa đổi dựa trên chính xác hàng 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 một URI nội dung đến, API nhà cung cấp sẽ bao gồm lớp tiện lợi UriMatcher. Lớp này ánh xạ các mẫu URI nội dung tớ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 URI nội dung bằng cách sử dụng các 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 ký tự số có độ dài bất kỳ.

Xem ví dụ về cách thiết kế và mã hoá cách xử lý URI nội dung, hãy cân nhắc sử dụng một 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 đây 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 các URI nội dung này nếu chúng có 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:

content://com.example.app.provider/*
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 bảng dataset1dataset2, nhưng không khớp với 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 cho thấy 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 trên 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 hàng đơn.

Phương thức addURI() liên kết một quyền 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 chọn truy vấn toàn bộ bảng hoặc 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 thuận tiện để làm việc với phần id của URI nội dung. Các lớp UriUri.Builder có 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 đố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 tập dữ liệu có cấu trúc bằng cách xử lý yêu cầu từ 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 đó, phương thức này sẽ gọi một phương thức cụ thể của ContentProvider để có 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() đều được gọi bởi một ứng dụng khách đang cố gắng 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. 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 của 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 trình cung cấp. Dùng các đối số để chọn bảng đích đến và lấy các giá trị trong cột cần 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 nhà cung cấp của bạn. Dùng các đối số để chọn bảng và hàng để cập nhật và lấy các giá trị đã cập nhật cho cột. Trả về số hàng đã cập nhật.
delete()
Xoá các hàng khỏi 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 xoá. Trả về số lượng hàng đã xóa.
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 các loại MIME của nhà cung cấp nội dung.
onCreate()
Khởi động trình cung cấp. Hệ thống Android sẽ gọi phương thức này ngay sau khi tạo ứng dụng. 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 đối tượng đó.

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

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:

  • Tất cả các phương thức này, ngoại trừ onCreate(), có thể được nhiều luồng gọi cùng một lúc, 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 kéo 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 triển khai phương thức onCreate() sẽ thảo luận chi tiết hơn về vấn đề 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 cần làm gì ngoài việc trả về kiểu 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 nơi lưu trữ 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 bất kỳ hàng nào, hãy trả về một thực thể 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 dùng cơ sở dữ liệu SQLite làm nơi lưu trữ dữ liệu, hãy 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 hàng mới.

Hệ thống Android phải có khả năng giao tiếp Exception qua ranh giới quy trình. Android có thể thực hiện việc này cho các trường hợp ngoại lệ hữu ích sau đây 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, 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 cho dòng mới. Để tạo giá trị 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 cách sử dụng withAppendedId().

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

Phương thức delete() không xoá hàng khỏi bộ nhớ dữ liệu của bạn. Nếu bạn đang 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 đã xoá và xoá các hàng đó khỏi máy chủ trước khi xoá khỏi 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ùng một đối số selectionselectionArgsdelete()ContentProvider.query() sử dụng. Việc 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 trì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 và trì hoãn việc tạo cơ sở dữ liệu cũng như 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 thao tác dài trong onCreate(), bạn sẽ làm chậm quá trình khởi động của ứng dụng nhà cung cấp. Đổi lại, việc này làm chậm phản hồi từ trình cung cấp đến 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 của ContentProvider

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

getType()
Một trong những phương thức bắt buộc mà bạn cần triển khai cho mọi nhà cung cấp.
getStreamTypes()
Một phương thức mà bạn dự kiến sẽ triển khai 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 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 MIME IANA.

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

  • Phần loại: vnd
  • Phần loại phụ:
    • Nếu mẫu URI chỉ dành cho một hàng: 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. Một lựa chọn phù hợp cho <name> là tên công ty hoặc một phần nào đó 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 thẩm quyền của trình cung cấp là com.example.app.provider và trình cung cấp đó 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 của 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 các tệp, hãy triển khai getStreamTypes(). Phương thức này 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. 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 cung cấp hình ảnh dưới dạng tệp ở định dạng JPG, PNG và GIF. Nếu một ứ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, thì ứng dụng có thể gọi ContentResolver.getStreamTypes() bằng chuỗi bộ lọc *\/jpeg, và getStreamTypes() trả về:

{"image/jpeg"}

Nếu trì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, 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 hằng số cho URI, tên cột, loại MIME và siêu dữ liệu khác liên quan đến trình cung cấp. Lớp này thiết lập 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 trình cung cấp có thể được truy cập chính xác ngay cả khi có các 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 có ích cho nhà phát triển vì lớp này thường có tên giúp ghi nhớ các hằng số. Vì vậy, 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ì đây là một lớp nên lớp 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ố đó.

Nhà phát triển không thể truy cập vào tệp lớp của lớp hợp đồng từ ứng dụng của bạn. Tuy nhiên, 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 các quyền của trình cung cấp nội dung

Bạn có thể xem mô tả chi tiết về các quyền và quyền truy cập vào mọi khía cạnh của hệ thống Android trong phần 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 là:

  • Theo mặc định, các tệp dữ liệu lưu trữ trên bộ nhớ trong của thiết bị là thông tin riêng tư của ứng dụng và nhà cung cấp của bạn.
  • Cơ sở dữ liệu SQLiteDatabase bạn tạo là riêng tư đối với ứng dụng và nhà cung cấp của bạn.
  • Theo mặc định, các tệp dữ liệu bạn lưu vào bộ nhớ ngoài sẽ ở chế độ công khaicó thể đọc được. 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 các lệnh gọi API khác để đọc và ghi chúng.
  • Với phương thức này, các lệnh gọi mở/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 cho cả quyền đọc và ghi đối với tất cả ứng dụng khác. Nếu bạn 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à bạn cấp cho ứng dụng đó quyền "có thể đọc được" hoặc "có thể ghi trong toàn cầu", thì các quyền bạn đặt cho nhà cung cấp đó 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 vào các tệp và cơ sở dữ liệu trong bộ nhớ trong là "riêng tư". Đừng thay đổi chế độ 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, thì 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, đồng thời đặt các tệp và cơ sở dữ liệu ở chế độ riêng tư cho ứng dụng của bạn.

Triển khai các quyền

Theo mặc định, tất 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 là riêng tư, vì theo mặc định, trình cung cấp của bạn không 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 trong tệp kê khai bằng cách 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, cho một số bảng nhất định, một số bản ghi hoặc cả ba.

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

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

Quyền ở cấp nhà cung cấp đọc-ghi duy nhất
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>.
Quyền đọc và ghi riêng biệt ở cấp nhà cung cấp
Quyền đọc và quyền ghi đối với toàn bộ trình cung cấp. Bạn chỉ định các thuộc tính này bằng thuộc tính android:readPermission android:writePermission của phần tử <provider>. Các 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 một 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à 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
Một 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ần đến. 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 duy nhất cần quyền vĩnh viễn cho nhà cung cấp 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 ứng dụng xem hình ảnh bên ngoài hiển thị tệp ảnh đính kèm từ nhà cung cấp của bạn. Để cấp cho trình xem hình ảnh quyền truy cập cần thiết mà không yêu cầu quyền, bạn có thể thiết lập các quyền tạm thời cho URI nội dung đối với ả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à gắn cờ quyền đến 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, ngay cả khi 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() bất cứ khi nào bạn xoá chế độ hỗ trợ cho một URI nội dung liên kết với một 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 lượng nội dung có thể truy cập của nhà cung cấp của bạn. 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à các quyền ở cấp đường dẫn hoặc cấp nhà cung cấp của bạ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 nội dung được cấp quyền truy cập tạm thời.

Để uỷ quyền truy cập tạm thời vào một ứng dụng, ý định phải chứa cờ FLAG_GRANT_READ_URI_PERMISSION, cờ FLAG_GRANT_WRITE_URI_PERMISSION hoặc cả hai. Các thuộc tính 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 coi 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 sử dụng phần tử <provider>. Hệ thống Android sẽ nhận những thông tin sau từ phần tử này:

Cơ quan cấp chứng nhận (android:authorities)
Tên tượng trưng xác định toàn bộ 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 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 các quyền của nhà cung cấp nội dung.

Thuộc tính khởi động và điều khiển
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 quy trình 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 trình cung cấp
  • android:exported: gắn cờ cho phép các ứng dụng khác dùng ứng dụng 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 trì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 về 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 nhà cung cấp:
  • android:icon: một tài nguyên có thể vẽ chứa biểu tượng của trì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: Cài đặt > Ứng dụng > Tất cả.
  • android:label: một nhãn thông tin mô tả nhà cung cấp, dữ liệu của nhà cung cấp đó hoặc cả hai. Nhãn này xuất hiện trong danh sách ứng dụng trong 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 về phần tử <provider>.

Ý đị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 bất kỳ phương thức nào của ContentResolver hoặc ContentProvider. Thay vào đó, phương thức 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 của chính 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 của đích đến 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 sử dụng để sửa đổi dữ liệu trong trình cung cấp.

Bạn có thể dùng quyền truy cập theo ý định để giúp dữ liệu trở nên toàn vẹn. 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 kinh doanh đượ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 trực tiếp sửa đổi 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 việc này một cách kỹ lưỡng. Giải thích vì sao quyền truy cập theo ý định bằng giao diện người dùng của ứng dụng hiệu quả hơn so với 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 bằng cách đọc 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.