建立內容供應器

內容供應器會管理資料中央存放區的存取權。您可以在 Android 應用程式中以一或多個類別實作供應器,以及資訊清單檔案中的元素。其中一個類別實作 ContentProvider 的子類別,這個子類別是供應器與其他應用程式之間的介面。

雖然內容供應器應該把資料提供給其他應用程式使用,但您可以在應用程式中設置活動,讓使用者查詢及修改由供應商管理的資料。

本頁面提供內容供應器的基本建構流程,以及要使用的 API 清單。

開始建構前的注意事項

開始建立供應器前,請考慮下列事項:

  • 決定是否需要內容供應者。如要提供下列一或多項功能,則必須建構內容供應器:
    • 您想要為其他應用程式提供複雜的資料或檔案。
    • 您想讓使用者將複雜的資料從自家應用程式複製到其他應用程式。
    • 您想要使用搜尋架構提供自訂搜尋建議。
    • 您想向小工具公開應用程式資料。
    • 您想實作 AbstractThreadedSyncAdapterCursorAdapterCursorLoader 類別。

    如果用量完全是在您自己的應用程式中,而且您不需要使用上述任何功能,則「不需要」供應商使用資料庫或其他類型的永久儲存空間。您可以改用「資料和檔案儲存空間總覽」一文所述的其中一種儲存系統。

  • 如果您尚未這麼做,請參閱 內容供應器基本概念,進一步瞭解提供者及其運作方式。

接下來,請按照下列步驟建立提供者:

  1. 為資料設計原始儲存空間。內容供應器以下列兩種方式提供資料:
    檔案資料
    通常會以檔案形式儲存在檔案中的資料,例如相片、音訊或影片。將檔案儲存在應用程式的私人空間。為了回應來自其他應用程式的檔案要求,您的供應商可以提供該檔案的控制代碼。
    「結構化資料」
    通常會流入資料庫、陣列或類似結構的資料。 將資料儲存成與由列和欄相容的格式。資料列代表一個實體,例如使用者或商品目錄中的商品。資料欄代表實體的部分資料,例如個人名稱或商品價格。儲存這類資料的常見方法是儲存在 SQLite 資料庫中,但您可以使用任何類型的永久儲存空間。如要進一步瞭解 Android 系統提供的儲存空間類型,請參閱「 設計資料儲存空間」一節。
  2. 定義 ContentProvider 類別的具體實作項目及其必要方法。這個類別是您的資料與其他 Android 系統之間的介面。如要進一步瞭解這個類別,請參閱「實作 ContentProvider 類別」一節。
  3. 定義供應器的授權字串、內容 URI 和資料欄名稱。如果您希望提供者的應用程式處理意圖,也請定義意圖動作、額外資料和旗標。此外,您也可以定義應用程式必須具備哪些權限,才能存取您的資料。建議在單獨合約類別中將所有這些值定義為常數。您之後可以將這個類別公開給其他開發人員。如要進一步瞭解內容 URI,請參閱設計內容 URI 一節。如要進一步瞭解意圖,請參閱「意圖和資料存取權」一節。
  4. 新增其他選用部分,例如範例資料或 AbstractThreadedSyncAdapter 實作,以便在供應商和雲端資料間同步處理資料。

設計資料儲存空間

內容供應器是指以結構化格式儲存的資料介面。在建立介面之前,請先決定資料儲存方式。您可以使用任何形式儲存資料,然後設計用於讀取及寫入資料的介面。

以下是 Android 可用的一些資料儲存技術:

  • 如果您使用的是結構化資料,請考慮使用關聯資料庫 (例如 SQLite),或 LevelDB 等非關聯鍵/值資料儲存庫。如果您使用的是音訊、圖片或影片媒體等非結構化資料,請考慮將資料儲存為檔案。您可以混合及比對不同類型的儲存空間,並視需要透過單一內容供應器公開。
  • Android 系統可與 Room 持續性程式庫互動,藉此提供 SQLite 資料庫 API 的存取權,供 Android 供應商用於儲存資料表導向資料。如要使用這個程式庫建立資料庫,請將 RoomDatabase 的子類別例項化,如「使用 Room 將資料儲存在本機資料庫」中所述。

    您不需要使用資料庫導入存放區。提供者會以一組資料表的形式顯示,這類似於關聯資料庫,但這不是供應商內部實作的必要條件。

  • 如要儲存檔案資料,Android 提供多種檔案導向 API。如要進一步瞭解檔案儲存空間,請參閱「資料和檔案儲存空間總覽」。如果您設計的供應商提供音樂或影片等媒體相關資料,您可以建立一個可合併資料表資料和檔案的供應商。
  • 在極少數情況下,建議您為單一應用程式實作多個內容供應器。例如,您可能想要使用一個內容供應器與小工具分享部分資料,並公開另一組資料來與其他應用程式共用。
  • 如要處理網路資料,請使用 java.netandroid.net 中的類別。您也可以將網路資料同步處理到本機資料儲存庫 (例如資料庫),然後以資料表或檔案的形式提供資料。

注意:如果您對存放區進行的變更無法回溯相容,則必須使用新的版本號碼標示存放區。您也必須針對實作新內容供應器的應用程式,增加版本號碼。進行這項變更可以防止系統在嘗試重新安裝含有不相容內容供應器的應用程式時當機,造成系統當機。

資料設計注意事項

以下為設計供應商資料結構的一些提示:

  • 資料表資料一律須有「主鍵」欄,由供應商維護為每一列的專屬數值。您可以使用這個值,將資料列連結至其他資料表中的相關資料列 (當做「外鍵」)。雖然您可以將任何名稱用於這個資料欄,但建議您使用 BaseColumns._ID,因為將提供者查詢的結果連結至 ListView,系統需要其中一個擷取的資料欄名稱就是 _ID
  • 如要提供點陣圖圖片或其他非常大的檔案導向資料,請將資料儲存於檔案然後間接提供,而不要直接將資料儲存在資料表中。如果您這麼做,就需要告知供應器使用者,他們需要使用 ContentResolver 檔案方法存取資料。
  • 使用二進位大型物件 (BLOB) 資料類型,儲存大小各異或具有不同結構的資料。舉例來說,您可以使用 BLOB 資料欄儲存通訊協定緩衝區JSON 結構

    您也可以使用 BLOB 實作獨立結構定義的資料表。在這類資料表中,您可以將主鍵資料欄、MIME 類型資料欄以及一或多個一般資料欄定義為 BLOB。BLOB 資料欄中資料的定義以 MIME 類型資料欄中的值表示。這樣就能將不同的資料列類型儲存在相同的資料表中。聯絡人提供者的「資料」表格 ContactsContract.Data 是與結構定義無關資料表的範例。

設計內容 URI

內容 URI 是用來識別供應器中資料的 URI。內容 URI 包含整個供應器的符號名稱 (其主機名稱),以及指向資料表或檔案的名稱 (路徑)。選用的 ID 部分會指向資料表中的個別資料列。ContentProvider 的每項資料存取方法都有內容 URI 做為引數。藉此決定要存取的資料表、資料列或檔案。

如需內容 URI 的相關資訊,請參閱「 內容供應器基本概念」。

設計權威人士

提供者通常設有單一授權,也就是其 Android 內部名稱。為避免與其他供應商發生衝突,請以供應商授權為基礎,反向使用網際網路網域擁有權。由於這項建議也適用於 Android 套件名稱,因此您可以將提供者授權定義為包含該供應商的套件名稱。

舉例來說,如果您的 Android 套件名稱為 com.example.<appname>,請將主機名稱提供給供應商 com.example.<appname>.provider

設計路徑結構

開發人員通常會透過附加指向個別資料表的路徑,從授權單位建立內容 URI。舉例來說,如果您有 table1table2 這兩個資料表,則可將這兩個資料表與上一個範例的授權合併,以產生內容 URI com.example.<appname>.provider/table1com.example.<appname>.provider/table2。路徑不限於單一區隔,且路徑各層級也沒有資料表。

處理內容 URI ID

按照慣例,提供者在 URI 結尾接受含資料列 ID 值的內容 URI,即提供對資料表中單一資料列的存取權。此外,按照慣例,提供者會比對 ID 值與資料表的 _ID 欄,並針對相符的資料列執行要求的存取權。

此慣例對於存取供應器的應用程式,有助於採用常見的設計模式。應用程式會向提供者執行查詢,並使用 CursorAdapterListView 中顯示產生的 CursorCursorAdapter 的定義規定 Cursor 中任一資料欄必須為 _ID

然後,使用者從 UI 中選取任一列,以便查看或修改資料。應用程式會從支援 ListViewCursor 中取得對應的資料列,取得這一列的 _ID 值,將其附加到內容 URI,並向供應器傳送存取要求。提供者接著可以對使用者選取的確切資料列執行查詢或修改。

內容 URI 模式

為協助您針對傳入內容 URI 選擇要採取的動作,提供者 API 提供便利的類別 UriMatcher,可將內容 URI 模式對應至整數值。您可以在 switch 陳述式中使用整數值,為符合特定模式的內容 URI 或 URI 選擇所需動作。

內容 URI 模式會使用萬用字元來比對內容 URI:

  • * 符合任何長度的任何有效字元字串。
  • # 會比對任何長度的數值字元字串。

如要設計及編寫內容 URI 處理功能,請考慮採用授權 com.example.app.provider 的供應商,可識別下列指向資料表的內容 URI:

  • content://com.example.app.provider/table1:名為 table1 的資料表。
  • content://com.example.app.provider/table2/dataset1:名為 dataset1 的資料表。
  • content://com.example.app.provider/table2/dataset2:名為 dataset2 的資料表。
  • content://com.example.app.provider/table3:名為 table3 的資料表。

如果提供者會附加內容 URI,就也可以辨識內容 URI,例如針對 table31 識別的資料列提供 content://com.example.app.provider/table3/1

可能的內容 URI 模式如下:

content://com.example.app.provider/*
比對供應器中的所有內容 URI。
content://com.example.app.provider/table2/*
比對 dataset1dataset2 資料表的內容 URI,但與 table1table3 的內容 URI 不相符。
content://com.example.app.provider/table3/#
比對 table3 中單一資料列的內容 URI,例如 content://com.example.app.provider/table3/6 代表 6 識別的資料列。

下列程式碼片段顯示 UriMatcher 中的方法運作方式。這個程式碼使用內容 URI 模式 content://<authority>/<path> 處理資料表,並對單一資料列使用 content://<authority>/<path>/<id>,以不同方式處理整份資料表的 URI,與單列 URI 不同。

addURI() 方法會將授權和路徑對應至整數值。match() 方法會傳回 URI 的整數值。switch 陳述式可選擇查詢整個資料表,或是查詢單一記錄。

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
    }

另一個類別 ContentUris 提供便利的方法,以便處理內容 URI 的 id 部分。UriUri.Builder 類別提供可剖析現有 Uri 物件及建構新物件的便利方法。

實作 ContentProvider 類別

ContentProvider 執行個體會處理其他應用程式發出的要求,藉此管理結構化資料集的存取權。所有形式的存取權最終都會呼叫 ContentResolver,接著呼叫 ContentProvider 的具體方法以取得存取權。

必要方法

抽象類別 ContentProvider 定義了六個抽象方法,您可以在具體子類別中實作這些方法。用戶端應用程式會呼叫所有這些方法 (onCreate() 除外),並由嘗試存取內容供應器的用戶端應用程式呼叫。

query()
向供應商擷取資料。使用引數選取要查詢的資料表、傳回的資料列和資料欄,以及結果的排序順序。將資料以 Cursor 物件的形式傳回。
insert()
在供應器中插入新資料列。使用引數選取目的地資料表,並取得要使用的資料欄值。為新插入的資料列傳回內容 URI。
update()
更新供應程式中現有的資料列。使用引數選取要更新的資料表和資料列,並取得更新的資料欄值。傳回更新後的列數。
delete()
從供應器中刪除資料列。使用引數選取資料表和要刪除的資料列。傳回已刪除的列數。
getType()
傳回與內容 URI 相對應的 MIME 類型。如要進一步瞭解此方法,請參閱「實作內容供應器 MIME 類型」一節。
onCreate()
請初始化供應器。Android 系統會在建立供應器後立即呼叫這個方法。在 ContentResolver 物件嘗試存取該供應器之前,系統不會建立提供者。

這些方法的簽章與名稱相同的 ContentResolver 方法相同。

導入這些方法時,需要考慮下列事項:

  • 這些方法 (onCreate() 除外) 可一次由多個執行緒呼叫,因此必須確保執行緒安全。如要進一步瞭解多個執行緒,請參閱 程序和執行緒總覽
  • 請避免在 onCreate() 中執行耗時較長的作業。將初始化工作延後到實際需要時再執行。相關詳細說明請參閱「實作 onCreate() 方法」一節。
  • 雖然您必須實作這些方法,但程式碼無須執行任何操作,但傳回預期的資料類型。舉例來說,您可以忽略對 insert() 的呼叫並傳回 0,避免其他應用程式將資料插入部分資料表。

實作 query() 方法

ContentProvider.query() 方法必須傳回 Cursor 物件,如果失敗,則擲回 Exception。如果您是以 SQLite 資料庫做為資料儲存空間,可以傳回 SQLiteDatabase 類別的其中一個 query() 方法傳回的 Cursor

如果查詢沒有任何相符的資料列,則傳回 getCount() 方法傳回 0 的 Cursor 例項。只有在查詢程序期間發生內部錯誤時,才會傳回 null

如果您不是使用 SQLite 資料庫做為資料儲存空間,請使用 Cursor 的其中一個具體子類別。舉例來說,MatrixCursor 類別會實作遊標,而其中的每列都是 Object 例項的陣列。透過這個類別,使用 addRow() 新增資料列。

Android 系統必須能夠跨程序邊界通訊 Exception。Android 可以對下列例外狀況進行這項操作,這對於處理查詢錯誤非常實用:

實作 insert() 方法

insert() 方法會使用 ContentValues 引數中的值,將新資料列新增至適當的資料表。如果 ContentValues 引數中沒有資料欄名稱,建議您在供應商程式碼或資料庫結構定義中提供預設值。

這個方法會傳回新資料列的內容 URI。如要建構這種結構,請使用 withAppendedId() 將新資料列的主鍵 (通常是 _ID 值) 附加至資料表的內容 URI。

實作 delete() 方法

delete() 方法不需要從資料儲存空間中刪除資料列。如果您與供應商搭配使用同步轉換介面,建議將已刪除的資料列標示為「delete」標記,而不要將資料列完全移除。同步轉換介面可以檢查已刪除的資料列,將其從伺服器中移除,然後再從供應器中刪除資料列。

實作 update() 方法

update() 方法使用與 insert() 所用的相同的 ContentValues 引數,以及 delete()ContentProvider.query() 使用的相同 selectionselectionArgs 引數。讓您在這些方法之間重複使用程式碼。

實作 onCreate() 方法

Android 系統會在啟動供應器時呼叫 onCreate()。此方法僅執行快速執行的初始化工作,並延後資料庫建立和資料載入作業,直到提供者實際收到資料要求為止。如果您在 onCreate() 中執行冗長的工作,會拖慢供應商的啟動速度。進而拖慢供應器對其他應用程式的回應速度。

以下兩個程式碼片段示範 ContentProvider.onCreate() Room.databaseBuilder() 之間的互動。第一個程式碼片段示範 ContentProvider.onCreate() 的實作方式,也就是建構資料庫物件並處理資料存取物件的位置:

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

實作 ContentProvider MIME 類型

ContentProvider 類別提供兩種傳回 MIME 類型的方法:

getType()
您為任何供應商實作的其中一項必要方法。
getStreamTypes()
如果供應商提供檔案,建議您導入的方法。

資料表的 MIME 類型

getType() 方法會傳回 MIME 格式的 String,說明內容 URI 引數傳回的資料類型。Uri 引數可以是模式,而非特定 URI。在這種情況下,系統會傳回與模式相符的內容 URI 相關聯的資料類型。

針對文字、HTML 或 JPEG 等常見資料類型,getType() 會傳回該資料的標準 MIME 類型。如需這些標準類型的完整清單,請造訪 IANA MIME 媒體類型網站。

如果內容 URI 指向資料表的列或列,getType() 會以 Android 供應商專屬的 MIME 格式傳回 MIME 類型:

  • 輸入部分:vnd
  • 子類型部分:
    • 如果 URI 模式適用於單一資料列:android.cursor.item/
    • 如果 URI 模式用於多個資料列:android.cursor.dir/
  • 供應商特定部分:vnd.<name><type>

    您需要提供 <name><type><name> 值在全域範圍內不重複,且 <type> 值是對應的 URI 模式專屬。建議您使用 <name> 做為公司名稱或應用程式 Android 套件名稱的一部分。建議您使用 <type> 字串,識別與 URI 相關聯的資料表。

舉例來說,如果供應器的授權為 com.example.app.provider,且公開了名為 table1 的資料表,則 table1 中多個資料列的 MIME 類型為:

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

針對 table1 的單一資料列,MIME 類型為:

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

檔案的 MIME 類型

如果供應商提供檔案,請實作 getStreamTypes()。 這個方法會針對供應程式可針對特定內容 URI 傳回的檔案傳回 String 類型的 MIME 類型。按 MIME 類型篩選器引數篩選您提供的 MIME 類型,只傳回用戶端想要處理的 MIME 類型。

舉例來說,有一家提供 JPG、PNG 和 GIF 格式的相片圖片的供應商。 如果應用程式使用篩選器字串 image/* 呼叫 ContentResolver.getStreamTypes(),如果要求是「圖片」,ContentProvider.getStreamTypes() 方法會傳回陣列:

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

如果應用程式只對 JPG 檔案感興趣,可以使用篩選器字串 *\/jpeg 呼叫 ContentResolver.getStreamTypes()getStreamTypes() 會傳回:

{"image/jpeg"}

如果供應器未提供篩選器字串要求的任何 MIME 類型,getStreamTypes() 會傳回 null

實作合約類別

合約類別是一種 public final 類別,包含 URI、資料欄名稱、MIME 類型,以及與供應器相關的其他中繼資料的常數定義。這個類別會在供應器和其他應用程式之間建立合約,這樣即使 URI 的實際值、資料欄名稱等值發生變更,提供者也能夠正確存取,確保供應器能夠正確存取。

合約類別也對開發人員有幫助,因為其常數通常具有記憶名稱,因此開發人員較不可能在資料欄名稱或 URI 中使用不正確的值。由於這是類別,因此可包含 Javadoc 說明文件。整合式開發環境 (例如 Android Studio) 可以自動完成合約類別的常數名稱,並顯示 Javadoc 中的常數。

開發人員無法透過您的應用程式存取合約類別的類別檔案,但他們可以使用您提供的 JAR 檔案,以靜態方式將該檔案編譯到應用程式中。

ContactsContract 類別及其巢狀類別是合約類別的範例。

實作內容供應器權限

如要進一步瞭解 Android 系統各個層面的權限和存取權,請參閱「安全性提示」。資料與檔案儲存空間總覽也說明瞭各種儲存空間採用的安全機制和權限。簡單來說,請注意以下幾點:

  • 根據預設,裝置內部儲存空間中的資料檔案僅供應用程式和供應商存取。
  • 您建立的 SQLiteDatabase 資料庫僅供應用程式和供應商使用。
  • 根據預設,您儲存至外部儲存空間的資料檔案為公開全世界可讀取的資料檔案。您無法使用內容供應器限制對外部儲存空間中的檔案存取權,因為其他應用程式可以使用其他 API 呼叫來讀取及寫入檔案。
  • 這個方法呼叫在裝置內部儲存空間中開啟或建立檔案或 SQLite 資料庫,可能同時授予所有其他應用程式的讀取和寫入權限。如果您使用內部檔案或資料庫做為供應商的存放區,並授予其「全球可讀取」或「可寫入」的存取權,則您在資訊清單中為供應商設定的權限並不會保護資料。內部儲存空間中檔案和資料庫的預設存取權為「私人」,請勿針對供應商的存放區變更這項設定。

如果您想使用內容供應器的權限控管資料存取權,請將資料儲存在內部檔案、SQLite 資料庫或雲端 (例如遠端伺服器) 中,並且確保檔案和資料庫不對外公開。

實作權限

根據預設,即使基礎資料屬於私人資料,所有應用程式仍能讀取或寫入供應器,這是因為提供者並未設定任何權限。如要變更這項設定,請使用 <provider> 元素的屬性或子元素,在資訊清單檔案中設定提供者的權限。您可以設定適用於整個提供者、特定資料表或特定記錄的權限。

您可以在資訊清單檔案中使用一或多個 <permission> 元素,定義供應器的權限。如要指派供應器專屬的權限,請為 android:name 屬性使用 Java 式範圍設定。例如,將讀取權限命名為 com.example.app.provider.permission.READ_PROVIDER

以下清單說明供應商權限的範圍,首先從適用於整個供應商的權限開始,再進一步細分。較精細的權限的優先順序高於範圍較大的權限。

單一讀取/寫入提供者層級權限
一個控制整個供應器讀取和寫入權限的權限,使用 <provider> 元素的 android:permission 屬性指定。
分開讀取和寫入供應器層級權限
提供整個供應器的讀取權限和寫入權限。方法是使用 <provider> 元素的 android:readPermission android:writePermission 屬性。這類要求的優先順序高於 android:permission 要求的權限。
路徑層級權限
讀取、寫入或讀取/寫入供應器中的內容 URI 權限。您可以透過 <provider> 元素的 <path-permission> 子元素指定要控制的每個 URI。針對您指定的每個內容 URI,您可以指定讀取/寫入權限、讀取權限、寫入權限或全部三種。讀取和寫入權限的優先順序高於讀取/寫入權限。此外,路徑層級權限的優先順序高於供應商層級權限。
臨時權限
這個權限層級可授予應用程式暫時存取權,即使應用程式本身沒有一般所需的權限也一樣。暫時存取權功能會減少應用程式在資訊清單中要求的權限數量。開啟臨時權限後,只有供應商需要永久權限的應用程式才會持續存取您所有的資料。

舉例來說,如果您實作電子郵件服務供應商和應用程式,並想讓外部圖片檢視器應用程式顯示來自供應商的相片附件,請考慮所需的權限。如果想在不要求權限的情況下授予圖片檢視器必要的存取權,可以為相片的內容 URI 設定臨時權限。

設計電子郵件應用程式,讓使用者想要顯示相片時,應用程式會將包含相片內容 URI 和權限旗標的意圖傳送給圖片檢視器。接著,圖片檢視器可以向電子郵件服務供應商查詢擷取相片,即使該檢視器沒有供應器的一般讀取權限也一樣。

如要啟用臨時權限,請設定 <provider> 元素的 android:grantUriPermissions 屬性,或是在 <provider> 元素中新增一或多個 <grant-uri-permission> 子元素。當您不再對供應器的臨時權限相關聯的內容 URI 支援時,請呼叫 Context.revokeUriPermission()

屬性值會決定您的供應者有多少供應商可供存取。 如果屬性設為 "true",系統會向整個供應商授予臨時權限,覆寫供應商層級或路徑層級權限所需的任何其他權限。

如果此標記設為 "false",請在 <provider> 元素中加入 <grant-uri-permission> 子元素。每個子元素都會指定已授予暫時存取權的內容 URI 或 URI。

如要將暫時存取權委派給應用程式,意圖必須包含 FLAG_GRANT_READ_URI_PERMISSION 標記和/或 FLAG_GRANT_WRITE_URI_PERMISSION 旗標。請使用 setFlags() 方法進行設定。

如果沒有 android:grantUriPermissions 屬性,系統會假設為 "false"

<provider> 元素

如同 ActivityService 元件,其應用程式資訊清單檔案會使用 <provider> 元素,在資訊清單檔案中定義 ContentProvider 的子類別。Android 系統會從元素取得下列資訊:

權威推薦 (android:authorities)
用於在系統中識別整個供應商的符號名稱。如要進一步瞭解這項屬性,請參閱「設計內容 URI」一節。
提供者類別名稱 (android:name)
實作 ContentProvider 的類別。如要進一步瞭解這個類別,請參閱「實作 ContentProvider 類別」一節。
權限
用於指定其他應用程式存取供應器資料所需的權限的屬性:

如要進一步瞭解權限及其對應的屬性,請參閱「實作內容供應器權限」一節。

啟動和控制屬性
這些屬性可決定 Android 系統啟動提供者的方式和時間、供應器的程序特性,以及其他執行階段設定:

如需這些屬性的完整記錄,請參閱 <provider> 元素指南。

資訊屬性
供應商的選用圖示和標籤:
  • android:icon:可繪製資源,包含供應器的圖示。圖示會顯示在「設定」>「應用程式」>「全部」的應用程式清單中,顯示在供應商標籤旁。
  • android:label:說明供應器和/或資料的資訊標籤。標籤會顯示在「設定」 >「應用程式」 >「全部」的應用程式清單中。

如需這些屬性的完整記錄,請參閱 <provider> 元素指南。

注意:如果指定 Android 11 以上版本,請參閱套件瀏覽權限說明文件瞭解進一步設定需求。

意圖和資料存取權

應用程式可以透過 Intent 間接存取內容供應器。應用程式不會呼叫 ContentResolverContentProvider 的任何方法。而是傳送啟動活動的意圖,該活動通常是供應器自己的應用程式的一部分。目的地活動負責在其 UI 中擷取及顯示資料。

視意圖中的動作而定,目的地活動也可能會提示使用者修改供應器的資料。意圖也可以包含目的地活動在 UI 中顯示的「額外」資料。因此,使用者可在使用這些資料修改供應器中的資料前,先行選擇變更資料。

您可以使用意圖存取權協助資料完整性。您的供應商可能需要根據明確定義的商業邏輯插入、更新及刪除資料。在這種情況下,讓其他應用程式直接修改您的資料可能會導致資料無效。

如要讓開發人員使用意圖存取權,請務必詳加說明。說明為何使用應用程式 UI 執行意圖存取比嘗試透過程式碼修改資料更好。

處理需要修改供應器資料的傳入意圖,其實與處理其他意圖一樣。如要進一步瞭解如何使用意圖,請參閱「意圖和意圖篩選器」。

如需其他相關資訊,請參閱「日曆供應器總覽」。