创建 content provider

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

虽然 content provider 旨在向其他应用提供数据,但您的应用中可以包含 activity,让用户可以查询和修改由提供程序管理的数据。

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

着手构建前的准备工作

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

  • 决定是否需要内容提供方。如果您想提供以下一项或多项功能,则需要构建内容提供程序:
    • 您想为其他应用提供复杂的数据或文件
    • 您希望让用户能够将复杂的数据从您的应用复制到其他应用中。
    • 您想使用搜索框架提供自定义搜索建议。
    • 您希望向微件公开应用数据。
    • 您想要实现 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 的应用时崩溃。

数据设计注意事项

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

  • 表数据必须始终包含一个“主键”列,提供程序会将该列保留为每行的唯一数值。您可以使用此值将该行与其他表中的相关行相关联(将其用作“外键”)。虽然您可以为此列使用任何名称,但使用 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,则提供程序也可识别这些内容 URI,例如 content://com.example.app.provider/table3/1 对应于 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 与单行的 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 类型。实现内容提供程序 MIME 类型部分对此方法进行了更详细的说明。
onCreate()
初始化您的提供程序。Android 系统会在创建提供程序后立即调用此方法。只有在 ContentResolver 对象尝试访问您的提供方时,系统才会创建该提供方。

这些方法与同名的 ContentResolver 方法具有相同的签名。

您在实现这些方法时需要考虑以下事项:

  • 所有这些方法(onCreate() 除外)都可以由多个线程同时调用,因此它们必须是线程安全的。如需详细了解多个线程,请参阅 进程和线程概览
  • 避免在 onCreate() 中执行冗长的操作。将初始化任务推迟到实际需要时进行。有关实现 onCreate() 方法的部分对此进行了更详细的说明。
  • 尽管您必须实现这些方法,但您的代码只需返回预期的数据类型,无需执行任何其他操作。例如,您可以阻止其他应用向某些表插入数据,只需忽略对 insert() 的调用并返回 0 即可。

实现 query() 方法

ContentProvider.query() 方法必须返回 Cursor 对象,如果失败,则会抛出 Exception。如果您使用 SQLite 数据库作为数据存储,则可以返回由 SQLiteDatabase 类的其中一个 query() 方法返回的 Cursor

如果查询与任何行都不匹配,则返回 Cursor 实例(其 getCount() 方法返回 0)。仅当查询过程中发生内部错误时,才返回 null

如果您不使用 SQLite 数据库作为数据存储,请使用 Cursor 的某个具体子类。例如,MatrixCursor 类实现的游标中每行都是一个 Object 实例的数组。对于此类,请使用 addRow() 添加新行。

Android 系统必须能够跨进程边界传达 Exception。Android 可以针对以下异常执行此操作,这些异常有助于处理查询错误:

实现 insert() 方法

insert() 方法会使用 ContentValues 参数的值向相应表中添加新行。如果 ContentValues 参数中不包含列名称,则您可能需要在提供程序代码或数据库架构中为其提供默认值。

此方法返回新行的内容 URI。如需构造此方法,请使用 withAppendedId() 将新行的主键(通常是 _ID 值)附加到表的内容 URI。

实现 delete() 方法

delete() 方法不必从数据存储中删除行,如果您将同步适配器与提供程序一起使用,请考虑为已删除的行添加“删除”标志,而不是完全移除该行。同步适配器可以检查是否存在已删除的行,并将其从服务器中移除,然后再将其从提供程序中删除。

实现 update() 方法

update() 方法采用的 ContentValues 参数与 insert() 相同的参数,以及 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 数据库是应用和提供程序的专用数据库。
  • 默认情况下,您保存到外部存储空间的数据文件是公共的,并且可供所有人读取。您无法使用内容提供程序来限制对外部存储空间中文件的访问,因为其他应用可以使用其他 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",请将 <grant-uri-permission> 子元素添加到 <provider> 元素中。每个子元素都会指定被授予临时访问权限的一个或多个内容 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> 元素指南中全面介绍了这些属性。

注意:如果您以 Android 11 或更高版本为目标平台,请参阅软件包可见性文档,了解更多配置需求。

Intent 和数据访问

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

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

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

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

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

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