创建内容提供程序

内容提供程序管理对中央数据存储库的访问。您可以将提供程序实现为 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自己的提供程序使用该API来存储面向表的数据库。要使用此库创建数据库,请实例化 RoomDatabase的子类,如使用Room在本地数据库中保存数据中所述。

    您无需使用数据库来实现存储库。提供程序在外部显示为一组表,类似于关系数据库,但这并非提供程序内部实现的要求。

  • 对于存储文件数据,Android提供各种面向文件的API。要了解有关文件存储的更多信息,请阅读数据和文件存储概述。如果您正在设计一个提供媒体相关数据(例如音乐或视频)的提供程序,则可以创建一个结合了表数据和文件的提供程序。
  • 在极少数情况下,您可能需要为单个应用程序实现多个内容提供程序。例如,您可能希望使用一个内容提供程序与小部件共享一些数据,并公开另一组数据以与其他应用程序共享。
  • 对于处理基于网络的数据,请使用java.netandroid.net中的类。您还可以将基于网络的数据同步到本地数据存储(例如数据库),然后将数据作为表或文件提供。

注意:如果您对存储库进行了不向后兼容的更改,则需要使用新的版本号标记存储库。您还需要增加实现新内容提供程序的应用程序的版本号。进行此更改可以防止系统降级导致系统在尝试重新安装具有不兼容内容提供程序的应用程序时崩溃。

数据设计注意事项

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

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

    您还可以使用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,则提供程序也会识别这些内容 URI,例如content://com.example.app.provider/table3/1表示table3中由1标识的行。

以下是可能的Content 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。

方法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

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

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

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

实现 insert() 方法

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

此方法返回新行的内容 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.
    }
}

实现内容提供程序 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

实现契约类

契约类是一个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 的读取、写入或读写权限。您可以使用` <path-permission> ` <provider> 元素的子元素来指定要控制的每个 URI。对于您指定的每个内容 URI,您可以指定读写权限、读取权限、写入权限或所有三个权限。读取和写入权限优先于读写权限。此外,路径级权限优先于提供程序级权限。
临时权限
一种权限级别,即使应用程序没有通常所需的权限,也能向应用程序授予临时访问权限。临时访问功能减少了应用程序在其清单中需要请求的权限数量。启用临时权限后,唯一需要您提供程序永久权限的应用程序是持续访问所有数据的应用程序。

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

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

要启用临时权限,请设置` android:grantUriPermissions ` <provider> 元素的属性,或向`<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> 元素

与`Activity 和`Service 组件一样,`ContentProvider 的子类在其应用程序的清单文件中使用` <provider> 元素定义。Android 系统从该元素中获取以下信息:

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

权限及其相应的属性将在实现内容提供程序权限 部分中详细描述。

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

这些属性在` <provider> 元素指南中进行了全面介绍。

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

这些属性在` <provider> 元素指南中进行了全面介绍。

注意:如果您面向 Android 11 或更高版本,请查看包可见性文档以了解进一步的配置需求。

意图和数据访问

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

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

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

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

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

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