创建 content provider

内容提供程序管理对中央数据存储区的访问。您可以将提供程序实现为 Android 应用中的一个或多个类,以及清单文件中的元素。其中一个类会实现 ContentProvider 的子类,它是提供程序与其他应用之间的接口。

尽管 content provider 旨在向其他应用提供数据,但您可以在应用中设置 activity,以允许用户查询和修改由提供程序管理的数据。

本页面包含构建 content provider 的基本流程和要使用的 API 列表。

着手构建前的准备工作

在开始构建提供程序之前,请考虑以下事项:

  • 决定是否需要内容提供方。如果您想提供以下一项或多项功能,则需要构建 content provider:
    • 您想为其他应用提供复杂的数据或文件
    • 您希望允许用户将复杂的数据从您的应用复制到其他应用。
    • 您想使用搜索框架提供自定义搜索建议。
    • 您希望向微件公开应用数据。
    • 您想要实现 AbstractThreadedSyncAdapterCursorAdapterCursorLoader 类。

    如果完全在您自己的应用中使用,并且您不需要上述任何功能,则需要提供程序即可使用数据库或其他类型的永久性存储空间。您可以改为使用数据和文件存储概览中介绍的某个存储系统。

  • 如果您尚未阅读 内容提供程序基础知识,可先阅读,以详细了解提供程序及其工作原理。

接下来,请按照以下步骤构建提供程序:

  1. 设计数据的原始存储空间。content provider 通过以下两种方式提供数据:
    文件数据
    通常存储在文件中的数据,如照片、音频或视频。将文件存储在应用的私有空间中。您的提供程序可以应其他应用发出的文件请求提供文件句柄。
    “结构化”数据
    通常存储在数据库、数组或类似结构中的数据。 以与行和列的表兼容的形式存储数据。行表示实体,如人员或商品目录中的商品。列表示实体的某项数据,例如人员的姓名或商品的价格。此类数据通常存储在 SQLite 数据库中,但您可以使用任何类型的持久性存储。如需详细了解 Android 系统中提供的存储类型,请参阅 设计数据存储部分。
  2. 定义 ContentProvider 类及其所需方法的具体实现。此类是您的数据与 Android 系统其余部分之间的接口。如需详细了解此类,请参阅实现 ContentProvider 类部分。
  3. 定义提供程序的授权字符串、内容 URI 和列名称。如果您想让提供程序的应用处理 intent,则还需要定义 intent 操作、extra 数据和标记。此外,还要定义要访问数据的应用所需的权限。请考虑在单独的协定类中将所有这些值定义为常量。稍后,您可以向其他开发者公开此类。如需详细了解内容 URI,请参阅设计内容 URI 部分。 如需详细了解 intent,请参阅 intent 和数据访问部分。
  4. 添加其他可选部分,例如示例数据或可在提供程序和云端数据之间同步数据的 AbstractThreadedSyncAdapter 实现。

设计数据存储

content provider 是以结构化格式保存的数据的接口。在创建该接口之前,您需要确定如何存储数据。您可以按自己的喜好以任何形式存储数据,然后根据需要设计读写数据的接口。

以下是 Android 支持的一些数据存储技术:

  • 如果您使用的是结构化数据,请考虑使用关系型数据库(例如 SQLite)或非关系型键值对数据存储区(例如 LevelDB)。如果要处理非结构化数据(如音频、图片或视频媒体),请考虑将数据存储为文件。您可以混合搭配多种不同类型的存储机制,并在必要时使用单个 content provider 公开它们。
  • Android 系统可以与 Room 持久性库进行交互,后者提供对 SQLite 数据库 API 的访问权限,Android 自己的提供程序使用该 API 来存储面向表的数据。如需使用此库创建数据库,请实例化 RoomDatabase 的子类,如使用 Room 将数据保存到本地数据库中所述。

    您无需使用数据库来实现代码库。提供程序在外部表现为一组表,类似于关系型数据库,但这不是对提供程序内部实现的要求。

  • 为了存储文件数据,Android 提供了多种面向文件的 API。如需详细了解文件存储,请参阅数据和文件存储概览。如果您要设计提供媒体相关数据(如音乐或视频)的提供程序,则可以创建可将表数据和文件的提供程序。
  • 在极少数情况下,您可以受益于为单个应用实现多个 content provider。例如,您可能希望使用一个 content provider 与 widget 共享一些数据,并公开另一组数据以与其他应用共享。
  • 如需处理基于网络的数据,请使用 java.netandroid.net 中的类。您还可以将基于网络的数据同步到本地数据存储区(如数据库),然后以表或文件的形式提供数据。

注意:如果您对代码库所做的更改不向后兼容,则需要使用新版本号标记代码库。您还需要为实现新的 content provider 的应用提高版本号。进行此更改可防止系统降级,以免系统在尝试重新安装具有不兼容 content provider 的应用时崩溃。

数据设计注意事项

以下是设计提供程序数据结构的一些提示:

  • 表数据必须始终具有一个“主键”列,提供程序将其作为每行中的唯一数值进行维护。您可以使用此值将该行链接到其他表中的相关行(将其用作“外键”)。虽然您可以对此列使用任何名称,但使用 BaseColumns._ID 是最佳选择,因为将提供程序查询的结果关联到 ListView 需要其中一个检索的列具有名称 _ID
  • 如果您想提供位图图像或其他非常庞大的文件导向型数据,请将数据存储在文件中,然后间接提供这些数据,而不是直接将其存储在表中。如果这样做,您需要告知提供程序的用户,他们需要使用 ContentResolver 文件方法来访问数据。
  • 使用二进制大对象 (BLOB) 数据类型存储大小或结构不同的数据。例如,您可以使用 BLOB 列来存储协议缓冲区JSON 结构

    您还可以使用 BLOB 来实现架构无关的表。在此类表中,您要将主键列、MIME 类型列以及一个或多个通用列定义为 BLOB。BLOB 列中数据的含义由 MIME 类型列中的值指示。这样,您就可以在同一表中存储不同的行类型。例如,联系人提供程序的“数据”表 ContactsContract.Data 就是一个独立于架构的表。

设计内容 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

按照惯例,提供程序通过接受末尾具有行 ID 值的内容 URI 来提供对表中单个行的访问。同样按照惯例,提供程序会将 ID 值与表的 _ID 列进行匹配,并对匹配的行执行请求的访问。

此惯例为访问提供程序的应用提供了一种常见的设计模式。该应用会对提供程序执行查询,并使用 CursorAdapterListView 中显示生成的 CursorCursorAdapter 的定义要求 Cursor 中的一列为 _ID

然后,用户从界面中选择显示的一行,以查看或修改数据。应用从支持 ListViewCursor 获取对应的行,获取该行的 _ID 值,将其附加到内容 URI,然后向提供程序发送访问请求。然后,提供程序便可对用户选择的确切行执行查询或修改。

内容 URI 模式

为了帮助您选择对传入的内容 URI 执行的操作,提供程序 API 添加了便捷类 UriMatcher,可将内容 URI 模式映射到整数值。您可以在 switch 语句中使用这些整数值,为与特定模式匹配的一个或多个内容 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(例如 table3 中由 1 标识的行的 content://com.example.app.provider/table3/1),则提供程序也会识别这些 URI。

可以使用的内容 URI 模式如下所示:

content://com.example.app.provider/*
匹配提供程序中的任何内容 URI。
content://com.example.app.provider/table2/*
匹配表 dataset1 和表 dataset2 的内容 URI,但不匹配 table1table3 的内容 URI。
content://com.example.app.provider/table3/#
匹配 table3 中单个行的内容 URI,如 content://com.example.app.provider/table3/6 以匹配由 6 标识的行。

以下代码段展示了 UriMatcher 中方法的工作原理。此代码处理整个表的 URI 与处理单行的 URI 不同,它对表使用内容 URI 模式 content://<authority>/<path>,对单行使用 content://<authority>/<path>/<id>

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() 以外的所有这些方法均由尝试访问您的 content provider 的客户端应用调用。

query()
从您的提供程序检索数据。使用参数选择要查询的表、要返回的行和列以及结果的排序顺序。将数据作为 Cursor 对象返回。
insert()
在您的提供程序中插入一个新行。使用参数选择目标表并获取要使用的列值。返回新插入的行的内容 URI。
update()
更新提供程序中的现有行。使用参数选择要更新的表和行,并获取更新后的列值。返回已更新的行数。
delete()
从提供程序中删除行。使用参数选择要删除的表和行。返回已删除的行数。
getType()
返回与内容 URI 对应的 MIME 类型。实现 content provider 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() 方法会返回以下数组:

{ "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 类及其嵌套类便属于协定类。

实现 content provider 权限

安全提示中详细介绍了 Android 系统各个方面的权限和访问权限。 数据和文件存储概览还介绍了各种存储类型的安全性和权限。简而言之,要点如下:

  • 默认情况下,存储在设备内部存储空间中的数据文件只有您的应用和提供程序是私有的。
  • 您创建的 SQLiteDatabase 数据库是您的应用和提供程序的私有数据库。
  • 默认情况下,您保存到外部存储空间的数据文件是公共文件和全局可读取的。您无法使用 content provider 来限制对外部存储空间中文件的访问,因为其他应用可以使用其他 API 调用来读取和写入文件。
  • 用于在设备的内部存储空间中打开或创建文件或 SQLite 数据库的方法调用可能会向所有其他应用同时授予读取和写入权限。如果您将内部文件或数据库用作提供程序的代码库,并为其授予“可全局读取”或“可全局写入”的访问权限,则您在其清单中为提供程序设置的权限不会保护您的数据。内部存储空间中文件和数据库的默认访问权限为“私有”;请勿针对提供程序的代码库更改此访问权限。

如果您想使用 content provider 权限来控制对数据的访问权限,请将数据存储在内部文件、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 和权限标志的 intent。然后,图像查看器可以查询您的电子邮件提供程序以检索照片,即使该查看器没有您的提供程序的正常读取权限也是如此。

如需启用临时权限,请设置 <provider> 元素的 android:grantUriPermissions 属性,或者向 <provider> 元素添加一个或多个 <grant-uri-permission> 子元素。每当从提供程序中移除对与临时权限相关联的内容 URI 的支持时,调用 Context.revokeUriPermission()

该属性的值决定了可以访问多少提供程序。如果将此属性设置为 "true",则系统会向整个提供程序授予临时权限,从而覆盖提供程序级或路径级权限所需的任何其他权限。

如果此标志设置为 "false",则向 <provider> 元素添加 <grant-uri-permission> 子元素。每个子元素都会指定授予临时访问权限的一个或多个内容 URI。

如需将临时访问权限委托给应用,intent 必须包含 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 类部分对该类进行了更详细的说明。
权限
用于指定其他应用访问提供程序的数据所必须具备的权限的属性:

如需详细了解权限及其对应的属性,请参阅实现 content provider 权限部分。

启动和控制属性
这些属性决定了 Android 系统启动提供程序的方式和时间、提供程序的进程特性以及其他运行时设置:

<provider> 元素指南中完整记录了这些属性。

信息属性
提供程序的可选图标和标签:
  • android:icon:包含提供程序图标的可绘制资源。该图标会显示在应用列表(依次点击设置 > 应用 > 全部)中的提供程序的标签旁边。
  • android:label:描述提供程序和/或其数据的信息标签。该标签会显示在应用列表(依次点击设置 > 应用 > 全部)中。

<provider> 元素指南中完整记录了这些属性。

Intent 和数据访问

应用可以通过 Intent 间接访问 content provider。应用不会调用 ContentResolverContentProvider 的任何方法。而是会发送一个启动某个 activity 的 intent,该 activity 通常是提供程序自有应用的一部分。目标 activity 负责检索和显示其界面中的数据。

根据 intent 中的操作,目标 activity 还可以提示用户对提供程序的数据进行修改。intent 可能还包含目标 activity 在界面中显示的“extra”数据。然后,用户可以选择先更改这些数据,然后再使用这些数据修改提供程序中的数据。

您可以使用 intent 访问来帮助确保数据完整性。您的提供程序可能依赖于根据严格定义的业务逻辑插入、更新和删除数据。在这种情况下,让其他应用直接修改您的数据可能会导致数据无效。

如果希望开发者使用 intent 访问,请务必提供详尽的说明。解释为什么使用应用界面的 intent 访问比尝试通过代码修改数据更好。

处理需要修改提供程序数据的传入 intent 与处理其他 intent 没有什么不同。您可以阅读 intent 和 intent 过滤器,详细了解如何使用 intent。

如需了解其他相关信息,请参阅日历提供程序概览