创建内容提供程序

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

虽然内容提供程序旨在使数据可供其他应用程序使用,但您可以在应用程序中包含活动,让用户查询和修改提供程序管理的数据。

此页面包含构建内容提供程序的基本流程以及要使用的 API 列表。

在您开始构建之前

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

  • **确定您是否需要内容提供程序。**如果您希望提供以下一项或多项功能,则需要构建内容提供程序
    • 您希望向其他应用程序提供复杂数据或文件。
    • 您希望让用户将复杂数据从您的应用复制到其他应用。
    • 您希望使用搜索框架提供自定义搜索建议。
    • 您希望将应用程序数据公开给窗口小部件。
    • 您希望实现 AbstractThreadedSyncAdapterCursorAdapterCursorLoader 类。

    如果使用仅限于您自己应用程序中的数据库或其他类型的持久性存储,并且不需要前面列出的任何功能,则您**不需要**提供程序。相反,您可以使用 数据和文件存储概述 中描述的存储系统之一。

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

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

  1. 设计数据的原始存储。内容提供程序通过两种方式提供数据
    文件数据
    通常存储在文件中的数据,例如照片、音频或视频。将文件存储在应用程序的私有空间中。响应其他应用程序对文件的请求时,您的提供程序可以提供对该文件的句柄。
    “结构化”数据

    通常存储在数据库、数组或类似结构中的数据。将数据存储为与行和列表格兼容的格式。一行表示一个实体,例如一个人或库存中的一个物品。一列表示实体的一些数据,例如一个人的姓名或物品的价格。存储此类数据的一种常见方法是使用 SQLite 数据库,但您可以使用任何类型的持久性存储。要了解 Android 系统中可用的存储类型,请参阅 设计数据存储部分。
  2. 定义 ContentProvider 类的具体实现及其必需的方法。此类是数据与 Android 系统其余部分之间的接口。有关此类的更多信息,请参阅实现 ContentProvider 类部分。
  3. 定义提供程序的权限字符串、内容 URI 和列名。如果希望提供程序的应用程序处理 Intent,则还需要定义 Intent 操作、额外数据和标志。还要定义访问数据的应用程序所需的权限。考虑在单独的契约类中将所有这些值定义为常量。稍后,您可以向其他开发人员公开此类。有关内容 URI 的更多信息,请参阅设计内容 URI部分。有关 Intent 的更多信息,请参阅Intent 和数据访问部分。
  4. 添加其他可选部分,例如示例数据或 AbstractThreadedSyncAdapter 的实现,该实现可以在提供程序和基于云的数据之间同步数据。

设计数据存储

内容提供程序是保存为结构化格式的数据的接口。在创建接口之前,请确定如何存储数据。您可以以任何喜欢的形式存储数据,然后设计接口以根据需要读取和写入数据。

以下是一些 Android 上可用的数据存储技术

  • 如果您正在处理结构化数据,则可以考虑使用关系数据库(如 SQLite)或非关系键值数据存储(如 LevelDB)。如果您正在处理非结构化数据(例如音频、图像或视频媒体),则可以考虑将数据存储为文件。如果需要,您可以混合和匹配多种不同类型的存储,并使用单个内容提供程序公开它们。
  • Android 系统可以与 Room 持久性库交互,该库提供对 SQLite 数据库 API 的访问,Android 自身的提供程序使用该 API 来存储面向表的的数据。要使用此库创建数据库,请实例化 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 列匹配,并对匹配的行执行请求的访问。

此约定有助于访问提供程序的应用程序的常见设计模式。应用程序对提供程序执行查询,并在 ListView 中使用 CursorAdapter 显示生成的 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 表示 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> 处理整个表的 URI 与处理单个行的 URI 的方式不同,对于表使用 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,然后 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

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

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

Android 系统必须能够跨进程边界传递 Exception。Android 可以对以下异常执行此操作,这些异常在处理查询错误时很有用

实现 insert() 方法

insert() 方法使用 ContentValues 参数中的值将新行添加到相应的表中。如果列名不在 ContentValues 参数中,您可能希望在您的提供程序代码或数据库模式中为其提供默认值。

此方法返回新行的内容 URI。要构造此 URI,请使用 withAppendedId() 将新行的主键(通常是 _ID 值)附加到表的 Content 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()。此方法返回一个 String 数组,其中包含您的提供程序可以为给定内容 URI 返回的文件的 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

实现一个契约类

合约类(Contract Class)是一个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

以下列表描述了提供程序权限的作用域,从适用于整个提供程序的权限开始,然后变得越来越细化。更细化的权限优先于作用域更大的权限。

单个读写提供程序级权限
一个控制对整个提供程序的读写访问权限的权限,使用 android:permission属性指定 <provider>元素。
单独的读取和写入提供程序级权限
整个提供程序的读取权限和写入权限。您可以使用 android:readPermission android:writePermission属性指定 <provider>元素。它们优先于 android:permission所需的权限。
路径级权限
提供程序中内容 URI 的读取、写入或读写权限。您使用 <provider>元素的 <path-permission>子元素指定要控制的每个 URI。对于您指定的每个内容 URI,您可以指定读写权限、读取权限、写入权限或全部三个权限。读取和写入权限优先于读写权限。此外,路径级权限优先于提供程序级权限。
临时权限
一个权限级别,即使应用程序没有通常所需的权限,也授予应用程序临时访问权限。临时访问功能减少了应用程序在其清单中需要请求的权限数量。启用临时权限后,唯一需要您提供程序的永久权限的应用程序是持续访问所有数据的应用程序。

例如,考虑一下如果您正在实现电子邮件提供程序和应用程序,并且想要让外部图像查看器应用程序显示来自您提供程序的照片附件,则需要哪些权限。为了在不需要权限的情况下向图像查看器提供必要的访问权限,您可以为照片的内容 URI 设置临时权限。

设计您的电子邮件应用程序,以便当用户想要显示照片时,该应用程序会向图像查看器发送一个包含照片的内容 URI 和权限标志的意图。然后,图像查看器可以查询您的电子邮件提供程序以检索照片,即使查看器没有您提供程序的正常读取权限。

要启用临时权限,请设置 android:grantUriPermissions属性 <provider>元素或将一个或多个 <grant-uri-permission>子元素添加到您的<provider>元素。每当您从提供程序中删除与临时权限关联的内容 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组件一样,ContentProvider的子类在其应用程序的清单文件中使用 <provider>元素进行定义。Android 系统从该元素获取以下信息

权限 (android:authorities)
在系统中标识整个提供程序的符号名称。此属性在设计内容 URI部分中进行了更详细的描述。
提供程序类名 (android:name)
实现ContentProvider的类。此类在实现 ContentProvider 类部分中进行了更详细的描述。
权限
指定其他应用程序必须具有的权限才能访问提供程序数据的属性

权限及其对应的属性在实现内容提供程序权限部分中进行了更详细的描述。

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

这些属性在 <provider>元素指南中进行了全面说明。

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

这些属性在 <provider>元素指南中进行了全面说明。

注意:如果您面向 Android 11 或更高版本,请查看包可见性文档以获取更多配置需求。

意图和数据访问

应用程序可以使用Intent间接访问内容提供程序。应用程序不会调用ContentResolverContentProvider的任何方法。相反,它会发送一个启动活动的意图,该活动通常是提供程序自身应用程序的一部分。目标活动负责在其 UI 中检索和显示数据。

根据 Intent 中的动作,目标 Activity 还可以提示用户修改提供程序的数据。Intent 也可能包含目标 Activity 在 UI 中显示的“额外”数据。然后,用户可以选择在使用这些数据修改提供程序中的数据之前更改这些数据。

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

如果您希望开发者使用 Intent 访问,请务必对其进行详尽的记录。解释为什么使用您的应用程序 UI 进行 Intent 访问比尝试使用他们的代码修改数据更好。

处理想要修改提供程序数据的传入 Intent 与处理其他 Intent 没有区别。您可以通过阅读Intent 和 Intent 过滤器来了解更多关于使用 Intent 的信息。

有关其他相关信息,请参阅日历提供程序概述