建立內容供應器

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

雖然內容供應器的用意是為其他應用程式提供資料,但是您可以在應用程式中讓活動讓使用者查詢及修改供應器管理的資料。

本頁麵包含建構內容供應器的基本程序和要使用的 API 清單。

開始建構之前

開始建立供應器之前,請先考量以下事項:

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

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

  • 如果尚未進一步瞭解供應商及其運作方式,請參閱 內容供應器基礎知識一文。

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

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

設計資料儲存空間

內容供應器是以結構化格式儲存資料的介面。建立介面之前,請先決定資料儲存方式。您可以根據自己的喜好以任何形式儲存資料,然後視需要設計介面,以便讀取及寫入資料。

以下是 Android 提供的部分資料儲存技術:

  • 如果您使用的是結構化資料,請考慮使用 SQLite 等關聯資料庫,或是非關聯鍵/值資料儲存庫,例如 LevelDB。如要使用音訊、圖片或影片媒體等非結構化資料,請考慮將資料儲存為檔案。您可以混合搭配多種不同類型的儲存空間,並視需要透過單一內容供應器提供。
  • Android 系統可以與 Room 持續性程式庫互動,以便存取 Android 自家供應商用來儲存資料表導向資料的 SQLite 資料庫 API。如要使用這個程式庫建立資料庫,請按照使用 Room 將資料儲存在本機資料庫一節的說明,建立 RoomDatabase 的子類別。

    不必使用資料庫實作存放區。供應器會對外顯示為一組資料表,類似於關聯資料庫,但並不是供應器的內部實作要求。

  • 為儲存檔案資料,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 附加了資料列 ID,供應器也會辨識這些項目,例如 content://com.example.app.provider/table3/1 代表 table31 識別的資料列。

可能的內容 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() 方法不必從資料儲存空間中刪除資料列。如要搭配供應商使用同步轉換介面,建議您以「刪除」標記將已刪除的資料列標示為「刪除」,而不要將列完全移除。同步轉換介面可以檢查已刪除的資料列,並從伺服器中移除,然後再從供應商服務中刪除資料列。

實作 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 傳回的檔案傳回 MIME 類型的 String 陣列。按照 MIME 類型篩選器引數篩選您提供的 MIME 類型,即可只傳回用戶端想要處理的 MIME 類型。

舉例來說,假設供應商提供 JPG、PNG 和 GIF 格式的相片圖片檔案。如果應用程式使用篩選字串 image/* 呼叫 ContentResolver.getStreamTypes(),針對「圖片」的項目呼叫 ContentProvider.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",請將 <grant-uri-permission> 子元素新增至 <provider> 元素。每個子元素都會指定要授予臨時存取權的內容 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> 元素指南。

意圖和資料存取權

應用程式可以使用 Intent 間接存取內容供應器。應用程式不會呼叫 ContentResolverContentProvider 的任何方法。而會改為傳送啟動活動的意圖,通常也是供應者自有應用程式的一部分。目的地活動負責擷取資料,並在使用者介面中顯示資料。

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

您可以使用意圖存取權來協助資料完整性。您的供應商可能需要按照嚴格定義的商業邏輯插入、更新及刪除資料。如果是這種情況,允許其他應用程式直接修改您的資料可能會導致資料無效。

如要讓開發人員使用意圖存取權,請務必詳實記錄。說明為何使用應用程式 UI 的意圖存取,比嘗試使用其程式碼修改資料更為有效。

處理想要修改供應器資料的連入意圖,與處理其他意圖並無不同。如要進一步瞭解如何使用意圖,請參閱「意圖和意圖篩選器」。

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