本文翻译自android官方文档,结合自己测试,整理如下。
Content providers能够管理结构化的数据集,封装数据,并且能够提供数据安全的机制。Content providers是一种标准的接口,能够跨进程数据共享。中文可以被称为内容提供器。
当我们想从content providers中获取数据时,我们可以使用ContentResolver对象为客户端访问该providers。ContentResolver对象能够和Content Provider实例进行通信,该实例是继承抽象类ContentProvider类的一个子类的实例。Content Provider接收来自客户端(ContentResolver对象)的请求数据,执行请求动作,返回请求结果。
若我们不打算和其他应用程序进行共享数据,则我们没有必要创建自己的content provider。若有这种打算的话,需要提供content provider,以便提供个性化的查询建议。同时,若你想从你的程序中复制粘贴复杂的数据或文件到其他程序的话,我们也应该提供content provider。
Android系统自身也包括管理视频,音频,图片,联系人等的content providers。我们的应用程序可以在满足条件的情况下使用这些content providers。
Content Provider是android应用程序的一部分,同样能够提供和数据交互的UI。然而,content providers主要用于被其它程序使用的。providers和providers客户端为数据提供了一个一致的标准的接口,可以处理跨进程通信和安全访问数据。
本章节中主要描述以下内容:
下面将以读取联系人provider讲解各个部分。
content provider可以通过一个或多个表将数据暴露给其他应用程序,该表类似关系数据库中的表。表中的一行表示一条数据记录,每一列表示该记录的一个类型取值。这个不用过多描述。
注意: provider不需要提供主键,但是若想和ListView关联时,必须提供一个名为_ID的主键。下面将有详细的介绍。
应用程序通过抽象类ContentResovler对象访问content provider中的数据,该ContentResovler对象和ContentProvider对象有同名的方法,能够对持久化存储数据进行CRUD(create,retrieve,update,and delete)。
在客户端程序进程中的ContentResovler对象和在拥有provider的程序中的ContentProvider对象自动处理跨进程通信。ContentProvider对象也作为数据集和数据的外部表现的抽象层。
当然,若要访问系统provider(通常自定义的provider也需要设置许可),必须要有相应的许可(permissions)。下面有详细介绍。例如读取联系人provider需要添加下列许可:
<uses-permission android:name="android.permission.READ_CONTACTS" />
而若想要写内容的话则需要:
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
若要查询provider表中的数据,我们可以调用ContentResolver.query()
,然后该方法就会调用ContentProvider实现类对象的query()
方法。例如下面查看联系人信息代码:
public void forResult(View v){
Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
if(intent.resolveActivity(getPackageManager()) != null){
startActivityForResult(intent,REQUEST_SELECT_CONTACT);
} else
Toast.makeText(this,"没有满足条件的activity",Toast.LENGTH_LONG).show();
}
上面代码是调用系统联系人程序,当我们点击某个联系人之后,系统联系人程序就会销毁并将带有联系人URI的Intent传递给我们的程序,我们能在onActivityResult()
中处理该Intent对象,代码如下:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(REQUEST_SELECT_CONTACT == requestCode && RESULT_FIRST_USER == requestCode){
// 获取被点击的联系人URI
Uri uri = data.getData();
Cursor cursor = getContentResolver().query(uri,null,null,null,null);
// 判断是否读取成功
if(cursor != null){
int indexName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME_PRIMARY);
while(cursor.moveToNext()){
tv_name.setText("姓名为:" + cursor.getString(indexName));
}
} else{
Toast.makeText(this,"读取联系人失败,",Toast.LENGTH_LONG).show();
}
} else{
Toast.makeText(this,"读取联系人失败,requestCode = " + requestCode + ",resultCode = " + resultCode,Toast.LENGTH_LONG).show();
}
}
关于返回的Cursor对象下面有详细的讲解,暂时不管。下面的表格中展示了query()
方法中的参数如何匹配SQL SELECT语句:
参数 | 对应SQL部分 | 描述 |
---|---|---|
Uri | FROM table_name | 指定查询应用程序下的table_name表 |
mProjection | select column1,column2… | 指定查询的列名 |
mSelectionClause | WHERE column1 = value | 指定查询的where约束条件 |
mSelectionArgs | - | 为where中的占位符提供取值 |
mSortOrder | ORDER BY column1,column2… | 指定查询结果的排序方式 |
内容URI给内容提供器中的数据建立一种唯一的标识符,主要包括权限(aythority)和路径(path)两部分。权限是标识provider名称,用作区分应用程序的provider,可以在manifest配置文件中的<provider>
标签中设置,通常以包名作为前缀;路径则是表的名称,用于区分程序中不同的表。这样就形成了内容URI。例如一个provider的权限为:com.sywyg.provider
,一个表名为:table1
。则最终的URI字符串为:content://com.sywyg.provider/table1
。其中content://
为协议,表示该URI为内容URI。在得到URI字符串之后,可以通过,下面的方法将其解析为Uri对象:
Uri uri = Uri.parse("content://com.sywyg.provider/table1");
大多数providers可以通过指定ID访问某一单行例如(访问第二行):
Uri uri = ContentUris.withAppendedId("content://com.sywyg.provider/table1",2);
注意:Uri和Uri.Builder类能够方便的通过字符串构造标准格式的Uri。而ContentUris类则能够方便地向URI中添加id(或从URI中解析id),例如上面的例子。
这部分将描述如何从provider中检索数据。
为了方便描述,将ContentResolver.query()
查询方法写在UI线程中,但是在实际中,应该在子线程中进行异步查询。想要在子线程中实现的方法之一是使用CursorLoader类(目前还未整理)。这一部分在官方文档的activity下面的Loader有详细的讲解,目前还未整理。
从provider中检索数据需要完成以下两步:
若要检索/查询数据,需要有read access
许可,我们不能在程序运行时设置许可。因此,必须在manifest配置文件中指定该许可,通过<uses-permission>
标签使用要访问的provider定义的许可。
例如我们想要获取联系人信息,则可以在我们的程序中manifest文件中:
<uses-permission android:name="android.permission.READ_CONTACTS"/>
通过上一步设置许可之后,我们就可以对provider中的表进行查询了。通过ContentResolver对象的query()
方法设置具体的查询语句,上面已经详细地讲过query()
的使用。
在操作数据时,小心SQL注入。例如输入一个query()
方法中的参数mSelectionClause
设置为:
String mSelectionClause = "var = " + mUserInput;
这样的话,就有可能导致恶意的SQL攻击,例如mUserInput
被赋值为`”nothing;DROP TABLE *;”,那么provider就有可能把所有的表都删除。
因此,我们应该通过占位符来给约束条件赋值,通过这种方式,占位符给出的值只作为查询的条件,而不会进行连接到SQL语句中。例如上面的输入可以通过下面这种方式:
String mSelectionClause = "var = ?";
String[] selectionArgs = {""};
selectionArgs[0] = mUserInput;
其中,selectionArgs为query()
的第四个参数。
即使provider不依赖SQL数据库,上述通过占位符指定的方式也是可以的。
query()
方法返回一个Cursor对象,这样我们就可以从Cursor中读取查询结果了。provider可能会限制访问特定的列,因此,可能导致访问不了特定的列。若查询结果为空的话Cursor中的方法getCount()
为0。若查询出现错误,结果会依赖于特定的provider,有的可能返回null,有的抛出异常。
由于Cursor是一行一行的,因此通过SimpleCursorAdapter将数据显示在ListView上是一个不错的选择。若要在ListView上显示,表中必须要有id列,ListView通过id检索。因此,通常来说providers需要提供id列。
我们知道query()
返回一个Cursor对象,我们可以从该对象中获取想要的数据。通过移动游标的方法遍历Cursor的所有行,示例代码如下:
// 确定列号,即要信息的列
int index = mCursor.getColumnIndex("column1");
/* * Only executes if the cursor is valid. * 只有当cursor非空才执行 */
if (mCursor != null) {
/* * Moves to the next row in the cursor. Before the first movement in the cursor, the * "row pointer" is -1, and if you try to retrieve data at that position you will get an * exception. * 移动到下一行,行号是从-1开始的,因此第一次移动到第0行。 */
while (mCursor.moveToNext()) {
// 读取index列中的数据
newWord = mCursor.getString(index);
// Insert code here to process the retrieved word.
...
// end of while loop
}
mCursor.close();
} else {
// Insert code here to report an error if the cursor is null or the provider threw an exception.
}
}
moveToNext()
方法将光标移动到下一行,然后通过getXXX(int)
就能获取对应列的行取值(是不是像迭代器Iterator的用法,,,,)。Cursor中有一系列的getXXX(int)
方法用于返回列中对应的值(列中是该类型的值),例如上面的getString()
。同样可以通过getType()
返回某列中MIME类型。Cursor中还有其他getXXX()
方法,例如上面的getColumnIndex("column1")
获取指定列的索引。
provider指定的许可在使用该provider的程序中必须要声明。许可能够保证用户知道程序想要获取哪种数据。若不设置许可的话,外部程序不能使用该provider。然而和provider同一个程序中的组件可以任意获取,即使在有许可的情况下。
可以在manifest配置文件中通过`设置许可。在android安装程序时,用户必须保证该程序请求的所有许可都授权,否则不能安装该程序。
通过ContentResolver.insert()
方法能够对provider中的表进行插入数据,返回该插入行的URI。例如:
// 接收插入返回值
Uri mNewUri;
// 将要插入的数据保存在ContentValues(内部是hashMap实现)
ContentValues mNewValues = new ContentValues();
// 设置每一列的值,参数为列和值
mNewValues.put("column1", "example");
// 插入到某个表中,参数为Uri和插入的ContentValues
mNewUri = getContentResolver().insert(uri,mNewValues);
通过ContentValues对象设置要插入表中的值,该对象中设置的值的类型不需要和实际列中的值的类型一致。若不想设置某列,可以通过putNull("column2")
设置该列为null。对于provider中的id不需要指定值,该值作为主键自动添加。
对于insert()
方法返回的Uri格式如下:
content://authority/table/行号id
通过这个Uri就能访问该行,可以通过ContentUris.parseId(uri)
获得id(就是截取带有id的uri的最后部分)。
通过ContentResolver.update()
可以对数据进行更新,若想清除值,只需设置为null。
简单的代码如下;
// 同样使用ContentValues设置更新的数据
ContentValues mUpdateValues = new ContentValues();
// 定义约束条件,更新第column1列值为example的行。。。。。
String mSelectionClause = "column1 = ?";
String[] mSelectionArgs = {"example"};
// 接收修改行。
int mRowsUpdated = 0;
mUpdateValues.putNull("column2");
mRowsUpdated = getContentResolver().update(
uri,
mUpdateValues,
mSelectionClause,
mSelectionArgs
);
update()
中的四个参数和前两个和insert()
的参数一样,后两个是约束条件及条件取值。
通过ContentResolver.delete()
可以删除数据,该方法接收三个参数:Uri,mSelectionClause,mSelectionArgs。和update()
方法中的参数相比只少了一个ContentValues对象。
上述CRUD拼成简单的SQL语句为:
Content Provider提供的类型如下:
其它的数据类型,provider使用长度为64KB字节数组BLOB(Binary Large OBject)存储数据。BLOB是数据库中用来存储二进制文件的字段类型。
provider同样支持MIME数据类型,用于表示定义的URIs。下面有详细讲解。
有三种可选的访问形式:
ContentResolver.applyBatch()
操作。下面详细介绍Batch和通过intents方式,至于异步查询,将在Activity中的Loaders文档中介绍。
当需要插入大量的行数据或者插入到多个表中时,Batch访问就非常有用。
可以通过ContentProviderOperation类实现一组访问,使用ContentResolver.applyBatch()
操作。
例如下面进行一组插入操作:
// 创建一组ContentProviderOperation对象,每一个对象代表一次CRUD操作
ArrayList<ContentProviderOperation> ops =
new ArrayList<ContentProviderOperation>();
int rawContactInsertIndex = ops.size();
// 通过ContentProviderOperation类中的Builder类的build()方法创建ContentProviderOperation对象
// 没记错的话这应该是建造者模式
ops.add(ContentProviderOperation.newInsert(uri)
.withValue("column1", "value1")
.withValue("column2", "value2")
.build());
getContentResolver().applyBatch(authority,ops);
通过Intent可以间接地访问content provider。即使在没有许可的条件下,我们可以通过Intent返回的结果实现间接访问。
在没有许可的情况下,我们可以通过发送intent给另一个有许可的程序,然后返回一个带URI许可的intent对象。这些指定的URI许可将会一直存在,直到接收许可的activity销毁。拥有永久许可的程序将授权临时许可,通过设置intent的flag属性:
FLAG_GRANT_READ_URI_PERMISSION
FLAG_GRANT_WRITE_URI_PERMISSION
注意这些flags不是读写provider,只是能访问URI本身的许可。
若我们的应用程序没有许可,但是我们仍然想通过intent显示另一个程序的数据。例如日历程序接收一个ACTION_VIEW型的intent,就能显示日期或事件。这种可以不用创建自己的UI来显示日历信息。发送intent的程序不需要是关联provider的程序。
provider可以在manifest文件中<provider>
标签属性<android:grantUriPermission>
定义URI许可,同时也可以定义<provdier>
子标签<grant-uri-permission>
。
例如,我们检索Contacts Provider中的联系人数据,即使没有READ_CONTACTS许可,也可以做到。可能我们只需要读取某个联系人的信息,因此不需要请求READ_CONTACTS许可来访问所有的联系人,可以让用户选择我们的程序可以读取哪个联系人。需要完成以下步骤:
通过startActivityForResult()
发送一个包含ACTION_PICK的action(setAction())和CONTENT_ITEM_TYPE的联系人的MIME类型(setType())的intent,代码如下:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setType(ContactsContract.RawContacts.CONTENT_ITEM_TYPE);
startActivityForResult(intent,RESULTCODE);
因为intent匹配联系人app的activity的intent过滤器,因此该activity将会显示在前台。
setResult(resultCode,intent)
设置intent并返回给我们的应用程序。该intent包括:用户选择的联系人的URI,FLAG_GRANT_READ_URI_PERMISSION
的flags。这些flags授予我们的程序有URI许可,能够读取URI指定的联系人。最后,该activity调用finish()
销毁。这个过程由联系人app完成。onActivityResult()
方法,该方法接收刚才传过来的intent。intent.getData()
获取Uri),即使我们没在manifest文件中设置读取许可。这种方式其实就是上面演示查询联系人信息的过程。
我们可以使用另外一个拥有许可的应用程序操作provider。例如,我们想向日历中插入一些事件,则可以通过ACTION_INSERT的intent启动日历程序,让日历程序进行插入。
MIME (Multipurpose Internet Mail Extensions) 是描述数据类型的因特网标准。
Content Providers能够返回标准的MIME媒体类型,或者自定义MIME类型字符串,或者两者。
MIME类型格式为:
type/subtype
例如知名MIME类型有text/html
有text类型和html子类型。
自定义MIME类型字符串,也被称为”vendor-specific”MIME类型,有更复杂的类型和子类型。对于多行来说类型通常是:
vnd.android.cursor.dir
对于单行来说:
vnd.androi.cursor.item
子类型是provider指定的,android内部的providers都有一些简单的子类型。例如当创建一个联系人的电话时,可以使用:
vnd.android.cursor.item/phone_v2
这里,子类型就是phone_v2。
其它provider都有自定义的子类型,依赖于provider的权限(autority)和路径。例如,权限为com.example.train2,包括表Line1,Line2和Line3。对于表Line1的URI:
content://com.example.trains/Line1
对应的MIME类型为:
vnd.android.cursor.dir/vnd.example.line1
或vnd.android.cursor.dir/vnd.com.example.train2.line1
对于Line2的第5行URI:
content://com.example.trains/Line2/5
对应的MIME类型为:
vnd.android.cursor.item/vnd.example.line2
或vnd.android.cursor.dir/vnd.com.example.train2.line2