前言
什么是 ContentProvider?有什么用?
简单来说,ContentProvider 就是一个向外界提供数据的接口。它可以向非同一进程提供数据。
如果你需要先其他应用提供数据,那就有必要开发 ContentProvider 了。Android 本身的内容提供程序可以用来管理音频、视频、图像和个人联系信息等数据。任何 Android 应用都可以来访问这些提供程序,但是会受到一定的限制。
ContentProvider 概览
ContentProvider 作为应用的一部分,我可以通过它来获取数据,当然,ContentProvider 被设计出来的主要的意图是为了跨应用访问数据。
- 内容提供程序的工作方式;
- 用于从
ContentProvider中获取数据的 API; ContentProvider中插入、更新或删除数据的 API;- 其他
ContentProvider的 API 功能。
访问 ContentProvider
应用通过 ContentResolver 对象和拥有 ContentProvider 对象可自动处理跨进程通信。
注意:想要访问
ContentProvider,你需要在应用的清单文件中请求特定的权限。
例如,如果你想从 用户字典 ContentProvider 中获取字词以及其区域设置的列表,需要通过调用 ContentResovler.query()。 query() 方法会调用用户字典 ContentProvider 中定义的 ContentProvider.query() 方法。
1 | // Queries the user dictionary and returns results |
内容 URI
内容 URI 用来在 ContentProvider 程序中标识数据的 URI.这个 URI 包括整个提供程序的符号名称和一个指向表名称的路径。此 URI 也是你通过 ContentResolver 访问 ContentProvider 的重要参数之一。
ContentResolver 对象会分析出 URI 的授权,并通过该授权与包含 ContentProvider 对象应用的系统表进行比较,比对无误后,ContentResolver 就可以将查询的参数分配给正确的 ContentProvider 了。
以上面的示例代码为例:
1 | content://user_dictionary/words |
其中,user_dictionary 就是 ContentProvider 提供的授权,words 字符串是表的路径。content:// 始终显示,并被解释为 内容 URI.
大部分 ContentProvider 允许你在 URI 的末尾追加 ID 值来访问表中具体行。例如下面的例子,我们可以检索 _ID 为 4 的行:
1 | Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4); |
通过追加 ID,我们不光能够执行查询操作,还可以执行更新和删除操作。
注意:
Uri和Uri.Builder类都分装了 根据字符串构建规范格式的URI对象的方法,我们可以直接拿来用。ContentUris类也封装了 可以将ID值轻松追加到URI后的方法。上面的示例代码中就是通过ContentUris.withAppendId()方法将ID追加到用户字典内容的URI后面的。
通过 ContentProvider 查询数据
想要从 ContentProvider 中查询数据,你需要执行以下操作:
1.请求对 ContentProvider 读取的读取访问权限;
2.定义将查询操作发送给提供程序的代码。
请求读取访问权限
你需要在清单文件中定义提供者程序准确的权限名称,这样用户在安装你的应用时,会隐式授予此权限。比如,用户字典提供程序 在其清单文件中定义了权限 android.permission.READ_USER_DICTIONARY,这样,如果你想要获取它的相关数据,你就需要请求该权限。
构建查询
在获取了相关权限后,我们需要来构建查询,看下面的示例代码:
1 | // A "projection" defines the columns that will be returned for each row. |
上面的代码中定义了一些访问 用户字典 ContentProvider 变量。
再来看看下面的代码,同样是以 用户字典 为例,告诉了我们如何使用 ContentResolver.query().
1 |
|
上面的查询就类似于下面的 SQL:
1 | SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC; |
防止恶意输入
如果 ContentProvider 提供的数据位于数据库中,那么我们在拼写 SQL 的时候,可能会被 SQL 注入:
我们来看下面的这个示例代码:
1 | // 将用户的输入拼接成 SQL 的查询子句 |
上面的这段代码会允许用户将恶意的 SQL 串连到 SQL 语句上。例如,用户可以为 mUserInput 输入 “nothing;DROP TABLE *;”,
那么就会生成这样的语句 var = nothing; DROP TABLE *;这样会给我们的应用带来灾难性的后果。
那么,我们应该如何避免此类问题的出现?
通过将 ? 作为可替代参数的选择子句以及一个选择参数的数组。执行这样的操作,用户输入将会受到查询约束,而不是解释为 SQL 语句的一部分。这样就可以避免恶意的 SQL 注入。
下面是正确的写法:
1 | String mSelectionClause = "var = ?"; |
选择参数数组:
1 | String[] selectionArgs = {""}; |
将用户输入置于选择参数数组中:
1 | // Set the selection argument to the user's input |
Note:即使我们的
ContentProvider提供的数据不在数据库中,我们也应该将?作为可替换选择子句和选择参数数组来提供问号值作为首选方式。
显示查询结果
ContentResolver.query() 方法总是会返回一个 Cursor。我们可以通过使用 Cursor 方法,来访问查询结果中的相关数据信息。
但如果没有与查询条件相匹配的行,则提供程序会返回 Cursor.getCount() 为 0(空游标)的 Cursor 对象。
下面是将 ContentProvider 返回的 Cursor 通过 SimpleCursorAdapter 与 ListView 关联示例代码:
1 | // Defines a list of columns to retrieve from the Cursor and load into an output row |
从查询结果中获取数据
如果我们不光是想显示查询的数据,而是想从 Cursor 中做一些其他操作,你需要在 Cursor 中循环访问行:
1 | // Determine the column index of the column named "word" |
Cursor 实现了包含了很多用于从对象中检索不同类型的数据的 “获取” 方法。例如,上面的示例代码中使用的 getString() 方法。另外,我们还可以通过 getType() 方法来获取指示列的数据类型的值。
内容提供程序权限
提供程序的应用可以指定第三方应用访问提供程序所必须的权限。这样,其他程序会请求访问提供程序所需的权限。如果,提供程序未提供任何权限,则其他应用无法访问提供程序的数据。当然这是针对不同进程的程序请求而言,对于在同一个进程的组件是具有完整的读写权限的。
比如,我们想要访问用户字典程序需要 android.permission.READ_USER_DICTIONARY 权限才能从中检索数据。
通过 ContentProvider 我们可以对其进行插入、更新,如果你想进行删除数据的操作,你还需要申请 android.permission.WRITE_USER_DICTIONARY 权限。
如何通过 ContentProvider 进行增删改操作
我们不但能够通过 ContentProvider 来进行查询操作,还能通过它来进行增删改操作。
增
我们通过调用 ContentResolver.insert() 方法来插入数据。它会插入数据后并返回内容 URI:
1 | // Defines a new Uri object that receives the result of the insertion. |
我们不需要添加 _ID 列,系统会自动维护此列。ContentProvider 会向添加的每个行分配唯一的 _ID 值,并用以作为主键。
insert() 方法会放回一下形式的 URI 给我们:
1 | content://user_dictionary/words/<id_value> |
这个 <id_value> 就是新插入的行的 _ID 内容。
注意:要想从
Uri中获取_ID的值,请调用ContentUris.parseId().
改
通过 ContentResolver.update() 来执行更新操作。我么只需将需要 ContentValues 对象传入。而对于想要清除的字段内容,只需将值置为 null 即可。
示例代码如下:
1 | // Defines an object to contain the new values to insert |
注意: 你应该在
ContentResolver.update()时检查用户输入,以防止 恶意输入。
删除
删除操作与查询操作类似,我们只需要指定条件即可,delete() 方法会将 删除的行数 返回给我们。
1 | // Defines selection criteria for the rows you want to delete |
ContentProvider 数据类型
ContentProvider 可以提供不同类型的数据:
- 整型
- 长整型
- 浮点型
- 长浮点型(双倍)
ContentProvider 还会维护其定义的每个内容的 MIME 数据类型信息。在使用包含复杂数据结构或文件的 ContentProvider 时,通常需要 MIME 类型。例如,联系人的 ContentProvider 中的 ContactsContract.Data 表会使用 MIME 类型来标记每行中存储的联系人数据类型。想要获取与内容 URI 对应的 MIME 类型,请调用 ContentResolver.getType();
ContentProvider 的替代形式
ContentProvider 的三种替代形式在应用开发过程中很是重要:
- 批量访问: 你可以通过
ContentProviderOperation类中的方法创建一批访问调用,再通过ContentResolver.applyBatch()应用他们。 - 异步查询: 你应该在单独线程中执行查询。执行方式之一是通过
CursorLaoder对象。 - 通过 Intent 访问数据: 虽然我们无法向
ContentProvider发送 Intent,但是可以向ContentProvider的应用发送 Intent,后者通常具有修改ContentProvider数据的最佳配置。
批量访问
批量访问 ContentProvider 适用于插入大量行,或通过同一方法调用在多个表中插入行,或者用于跨进程界限将一组操作事务处理执行。
想要在 “批量模式” 下访问 ContentProvider,你可以创建 ContentProviderOperation 对象数组,然后使用 ContentResolver.applyBatch() 将其分派给内容提供程序。
我们需要将 ContentProvider 的 授权 传递给此方法,而不是特定内容的 URI.这样可以使数组中的每个 ContentProviderOperation 对象都能适用于其他表。
ContactsContract.RawContacts 协定类的说明包括展示批量注入的代码段。
通过 Intent 访问数据
我们可以通过 Intent 对 ContentProvider 进行间接的访问。即使我们的应用不具备访问权限。我们可以从具有权限的应用中获取回结果 Intent,或者通过激活具有权限的应用,然后让用户在其中工作。
通过临时权限获取访问权限:
即便我们没有适当的访问权限,还是可以通过以下方式来访问 ContentProvider 中的数据:
将 Intent 发送至具有权限的应用,然后接受到包含 URI 权限的结果 Intent. 这些就是特定内容的 URI 权限,此权限的生命周期将持续到 接受该权限的组件结束为止。具有永久权限的应用将通过在结果 Intent 中设置标志来授予临时权限:
- 读取权限:
FLAG_GRANT_READ_URI_PERMISSION - 写入权限:
FLAG_GRANT_WRITE_URI_PERMISSION
注意:这些标志不会为其授权包含在内容 URI 中的提供程序提供常规的读取或写入访问权限。访问权限仅适用于 URI 本身。
ContentProvider 使用 <provider> 元素的 android:grantUriPermission 属性以及 <provider> 元素的 <grant-uri-permission> 在清单文件中定义内容 URI 的 URI 权限。详情参见《安全与权限》。
举个例子,即使你没有 READ_CONTACTS 权限,也可以在联系人的 ContentProvider 中检索联系人的数据。比如说你希望在一个 向联系人发送电子生日祝福 的应用中执行此操作。你更希望让用户控制应用所使用的联系人,而不是请求 READ_CONTACTS,让你的应用能够访问用户所有的联系人和信息,这样做是不友好的,用户也很有可能因为此举而删掉你的应用。
如何做到呢?
1.使用 startActivityForResult() 发送包含操作 ACTION_PICK 和 联系人 MIME 类型 CONTENT_ITEM_TYPE 的 Intent 对象。
2.通过此 Intent 与 联系人 应用的 选择 Activity 的 Intent 过滤器相匹配,来显示 联系人 Activity.
3.用户在 联系人 界面上选择需要发送电子生日祝福的联系人。这样,该 Activity 会调用 setResult(resultcode, intent) 来设置用于返回的 Intent. Intent 包含用户选择的联系人的内容 URI,以及 extras 标志 FLAG_GRANT_READ_URI_PERMISSION。这些标志会为你的应用授予读取内容 URI 所指向的联系人的数据的 URI 权限。接着,联系人选择 Activity 会调用 finish() 以返回对应用的控制。
4.在 onActivityResult() 方法中,我们会接受到联系人应用选择界面所返回的 Intent.
5.通过 Intent 中的内容 URI,我们可以读取来自联系人 ContentProvider 的联系人数据,即使你未在清单文件中请求对该提供程序的永久读取访问权限。
6.最后,我们就可以在不获取联系人权限的前提下,向某个联系人发送生日祝福的功能了。
小结:这么麻烦,有必要吗?直接在清单文件中申请永久权限不就行了吗?用得着这么大费周章吗?
这么做是非常有必要的,你需要知道的是,用户对于 APP 申请读取敏感信息权限是非常反感的,这样做,很可能导致用户拒绝安装此应用,进而流失用户。
使用其他应用:
第二种方法允许用户修改你无权访问的数据的简单方法是激活具有权限的应用,让用户在其中执行工作。
例如,日历应用 接受 ACTION_INSERT Intent,这让你可以激活应用的插入 UI.你可以在此 Intent 中传递额外的数据,由于定期事件具有复杂的语法,因此将事件插入日历提供程序的首选方式是激活具有 ACTION_INSERT 的日历应用,然后让用户在其中插入事件。
协定类
协定类的作用是帮助应用使用内容 URI,列名称,Intent 操作以及内容提供程序的其他功能的常量。然而协定类未自动包含在提供程序中。开发者需要在 ContentProvider 中定义它们,然后使其可用于其他开发者。Android 平台中包含的许多提供程序都在软件包 android.provider 中具有对应的协定类。
例如,用户字典提供程序具有包含内容 URI 和 列名称常量的协定类 UserDictionary.字词表的内容 URI 在常量 UserDictionary.Words.CONTENT_URI 中定义。 UserDictionary.Words 类包含列名称常量,上面的示例代码就使用了相关常量。
联系人的 ContentProvider 的 ContactsContract 也是一个协定类。
MIME 类型引用
ContentProvider 可以返回标准的 MIME 媒体类型和 自定义 的 MIME 类型字符串。
MIME 类型具有的格式如下:
1 | type/subtype |
例如,text/html,ContentProvider 一旦返回此类型,则意味着使用该 URI 查询会返回包含 HTML 标记的文本。
自定义 MIME 类型(一般是特定供应商会使用)字符串具有更加复杂的类型和子类型,类型值始终为:
1 | vnd.android.cursor.dir |
上面表示 多行;
1 | vnd.android.cursor.item |
上面表示 单行;
子类型 特定于提供程序。Android 内置提供程序通常具有简单的子类型。例如,当联系人应用为电话号码创建行时,它会在行中设置以下 MIME 类型:
1 | vnd.android.cursor.item/phone_v2 |
上面的子类型就是 phone_v2.
其他程序的开发者可能会根据提供程序的授权和表名称创建自己的子类型模式。例如,假设提供程序包含了列车时刻表。且授权是 com.example.trains,并包含表 Line1,Line2 和 Line3.在响应 Line1 的内容 URI :
1 | content://com.example.trains/Line1 |
时,提供程序会返回 MIME 类型:
1 | vnd.android.cursor.dir/vnd.example.line1 |
在响应表 Line2 中的第五行的内容的 URI:
1 | content://com.example.trains/Line2/5 |
时,会返回的 MIME 类型为:
1 | vnd.android.cursor.item/vnd.example.line2 |
基本上大多数 ContentProvider 都会为其使用的 MIME 类型定义协定类常量。例如,联系人提供程序的协定类 ContactsContract.RawContacts 会为单个原始联系人行的 MIME 类型定义常量 CONTENT_ITEM_TYPE.
具体参见:《内容 URI》.
本文学习自:
https://developer.android.com/guide/topics/providers/content-provider-basics.html