Content Provider是Android四大组件之一,用于管理应用程序访问结构化的数据。Content Provider可以压缩数据,并保护访问数据的安全。Content Provdider是应用程序跨进程数据的标准接口。
使用Content Provider访问数据,你应当在自己的应用程序的Context环境中使用ContentResovler对象,并将该对象作为client对象,与content provider通信(实际上是与content provider的子类进行通信):content provider 接受cilent端发送的请求,并根据请求执行操作,最后将结果返回client端。
如果你不打算把自己应用程序的数据分享给其他应用,那么无需创建自己的content provider。但是,如果需要在自己的程序中提供定制的搜索规范,或是自己的应用向其他应用程序拷贝或粘贴大型而复杂的数据文件,那么就需要创建content provider。
Content Provider基础
provider是Android应用程序的一部分,使用provider提供的数据可以用于UI展示,然而,provider最常用的情形是将本应用的数据提供给其他应用程序使用,使用provider的client(ContentResovler)来访问provider,也就是说,provider一般与resolver配对使用,provider提供了访问数据的标准接口,可以方便resolver进行安全的跨进程通信
请注意:provider提供的表不一定必须提供组件,即便提供组件,名字也不一定必须是_ID,但是,若你需要将provider提供的表绑定到ListView上的话,那么该表必须提供主键,且名字必须是_ID.
1、访问provider
在应用程序中使用ContentResolver对象,可以访问其他应用程序的provider,在该类提供了与ContentProvider类中命名相同的方法,包括增删改查等。
ContentResolver所在进程与ContentProvider是不同的进程,所以这无形中实现了跨进程通信,而ContentProvider实际上还充当了抽象层,该层介于数据库和外部提供数据接口之间。
注意:为了使Content Resolver能访问Content Provider的数据,你需要在清单文件中加入访问权限。
举例来说,若需要获得上表中word和locale列的数据,需要在你的应用程序中调用ContentResolver.query方法,接着该方法又会调用ContentProvider.query()方法,下面演示了访问过程:
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI,
mProjection,
mSelectionClause
mSelectionArgs,
mSortOrder);
uri:表的uri地址
projection:要查询的列
selection:制定要选择的行
selectionArgs:补充前面的字符
sortOrder:排序
2、内容URI:
内容URI指向了provider提供的一组数据,内容URI由两部分组成,其中authority部分指定了需要访问的provider名,path部分指定了该provider的某张表。
在上述代码中,Content_URI指向了单词表,ContentResolver从Content_URI中解析出authority,并在系统中查询能与之相配的provider,找到该provider后,该provider利用URI的path部分寻找匹配的表,所以,综上所述,word表应该是如下uri:
content://user_dictionary/words
其中user_dictionary是authority,words是path,content://则是固定的,表示这是一个内容uri。
你也可以在URI后缀一个_ID来查询表中的某一行数据
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
这表示希望查询单词表中_ID为4的那一行数据,当你需要删除或者更新某一行数据时,应当使用这种加入_ID的URI
请注意:Uri类与Uri.Builder类提供了便捷的方法,把String包装为格式规范的URI对象,而ContentUri类同样提供了便捷的方法,在URI后追加ID,上述代码的withAppendedId()就是为Uri追加ID的方法。
3、从Provider中查询数据
为了清晰起见,本节调用的ContentResolver.query()都在UI线程中进行,但在实际开发中,这应该是一个异步操作--方法应该在子线程中进行,方法之一是使用CursorLoader。
从provider中查询数据,应按照如下操作进行
(1)声明读取数据的访问权限
(2)发送查询请求
//要查询的列
String[] mProjection = {
UserDictionary.Words._ID,
UserDictionary.Words.WORD,
UserDictionary.Words.LOCALE
};
//要查询的项
String mSelectionClause = null;
//查询项的字符
String[] mSelectionArgs = {""};
public class MainActivity extends AppCompatActivity {
Cursor mCursor;
//要查询的列
String[] mProjection = {
UserDictionary.Words._ID,
UserDictionary.Words.WORD,
UserDictionary.Words.LOCALE
};
//要查询的项
String mSelectionClause = null;
//查询项的字符
String[] mSelectionArgs = {""};
TextView mSearchWord;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//从UI中获取String
String mSearchString= mSearchWord.getText().toString();
//记得用代码来检查无效的或者恶意的输入
//如果该String不为空,获取所有数据
if (TextUtils.isEmpty(mSearchString)) {
//把mSelectionClause设置为null返回所有数据
String mSelectionClause = null;
mSelectionArgs[0] = "";
}
else {
//根据用户输入的词来构建一个选择项
mSelectionClause=UserDictionary.Words.WORD+"=?";
mSelectionArgs[0] = mSearchString;
}
//进行查询并返回Cursor
mCursor=getContentResolver().query(
UserDictionary.Words.CONTENT_URI,
mProjection,
mSelectionClause,
mSelectionArgs,
null);
//如果有错误发生一些provider会返回null,
if (mCursor == null) {
//在这里用代码处理错误,并且一定不能使用cursor
}
//如果cursor是空的,说明没有匹配的结果
else if (mCursor.getCount()<1){
//在这里插入代码来通知用户搜索失败,这不一定是个错误,可以提示用户插入一个新的搜索词
}
else {
//在这里返回查询结果
}
}
}
考虑到下面这种情况
Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause = "var = " + mUserInput;
为避免上述情况,应使用?作为占位符,以分离SQL语句和条件筛选参数,因为这种输入方式,系统将不再把用户的输入作为SQL语句的一部分,用户无法再恶意破坏数据库内容。
(4)显示查询结果
client端的ContentResovler.query()方法返回一个Cursor对象,他指向了按筛选条件查询的结果,调用Cursor中的方法,你可以遍历结果中的每一行,并通过表的字段名得到该行中的每一列信息,当provider的数据发生改变时,Cursor的数据自动改变,或者触发observer对象中的方法。
若没有行能匹配查询条件,Cursor.getcount()将返回0。
若查询过程发生了错误,Cursor对象将返回null,或者抛出一个异常.。
由于Cursor返回的是一个列表,可以通过simpleCursorAdapter将列表内容显示在ListView上,如下所示:
//列名
String[] mWordListColumns={
UserDictionary.Words.WORD,
UserDictionary.Words.LOCALE
};
//定义视图id
int mWordListItems={R.id.dictWord, R.id.locale};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//simpleCursorAdapter
CursorAdapter mCursorAdapter=new SimpleCursorAdapter(
getApplicationContext(), //context对象
R.layout.wordlistrow, //一行的xml布局文件
mCursor, //返回的cursor
mWordListColumns, //列名
mWordListItems, //每行的视图id
0 //flag(一般不需要)
);
mWordList.setAdapter(mCursorAdapter);
(5)从Cursor中获取数据
下面的代码演示了如何从Cursor中获取Word字段中的数据
public void getData(){
//定义word列是第几列
int index=mCursor.getColumnIndex(UserDictionary.Words.WORD);
/**
* 只有cursor是有效的才会执行,一些provider在发生错误时会返回null,而另外一些可能抛出异常
*/
if (mCursor!=null){
/**
* 光标移动到下一行,在移动到底一行之前,行指针为-1,如果你想在这个位置获取数据的话会得到一个异常
*/
while (mCursor.moveToNext()){
//获取值
String word = mCursor.getString(index);
//插入代码来检索词
}
}
else {
//插入代码来处理错误
}
}
在应用程序设置中,需要为provider设置权限,其他应用程序只有获得了权限,才能访问provider。为provider设置权限保证了provider知道访问他的应用程序需要获得什么数据,这保证了provider所管理的数据安全。
如果provider未制定任何权限,其他应用将无法访问,不过,无论provide是否设置了权限,与provider处于同一应用的组件有完全的读写provider权限。
正如上面提到的,用户字典需要android.permission.READ_USER_DICTONARY权限才能被其他应用所访问,当然,改权限只是具有读取provider的权限,如需要对provider所管理的数据进行增、删、改的操作还需要声明写入权限。
声明这些权限需要在清单文件的
(7)增删改数据
与查询类似,使用resolver同样可以对provider进行操作
1)插入数据
调用ContentResolver.insert()方法可以向provider中插入数据,该方法向provider中的某个表插入一行数据,并将该行数据的内容URI返回,下面为演示:
public void add(){
//定义一个新的uri对象来接收插入的结果
Uri mUri;
//定义一个对象来包含要插入的新值
ContentValues mNewValues=new ContentValues();
/*
把值设置给每一列并且插入word,这两个值分别是列名和值
*/
mNewValues.put(UserDictionary.Words.APP_ID,"example.user");
mNewValues.put(UserDictionary.Words.LOCALE,"en_US");
mNewValues.put(UserDictionary.Words.WORD,"insert");
mNewValues.put(UserDictionary.Words.FREQUENCY,"100");
mUri=getContentResolver().insert(
UserDictionary.Words.CONTENT_URI,//用户字典的uri
mNewValues);
}
在增加一行时,无需指定_ID字段的值,因为在一般情况下,将_ID设为主键和自增长,该字段将被自动赋予值。
返回的uri格式如下:
content://user_dictionary/words/
上面的id_value表示新加入的id号,如需从返回的Uri对象中获取该ID值,可以调用ContentUri.parseId()。
2)更新数据
在client端调用ContentResovler.update()方法可以更新一条数据,你同样可以使用ContentValue对象修改数据,如需将某一字段下的数据清空,直接传入null即可。
下面演示了将provider管理的用户字典中的local字段含有“en”的数据条目清空,返回值是修改的行数:
public void update(){
ContentValues mUpdateValues=new ContentValues();
//定义想要修改哪些行
String mSelectionClause=UserDictionary.Words.LOCALE+"LIKE ?";
String[] mSelectionArgs={"en_%"};
//定义一个变量来接收修改的行数
int mRowsUpdated=0;
mUpdateValues.putNull(UserDictionary.Words.LOCALE);
mRowsUpdated=getContentResolver().update(
UserDictionary.Words.CONTENT_URI,
mUpdateValues,
mSelectionClause,
mSelectionArgs
);
}
调用ContentResolver.delete删除表中指定的数据,下面演示了删除表中appid字段中包含user的条目:
public void delete(){
//定义想要修改哪些行
String mSelectionClause=UserDictionary.Words.APP_ID;
String[] mSelectionArgs={"user"};
//定义一个变量来接收删除的行数
int mRowsDeleted=0;
mRowsDeleted=getContentResolver().delete(
UserDictionary.Words.CONTENT_URI,
mSelectionClause,
mSelectionArgs
);
}
除了用户字典中的text类型,provider还支持下列类型操作:
integer
long
float
double
provider还经常使用BLOB(Binary Large OBject),它是最大容量为64KB的字节数组,他可以查询Cursor类中若干get方方法来查看provider支持的类型
provider为每一个访问或返回cilent端的内容URI对应了一个MIME数据类型,通过该MIME数据类型可以判断你的应用程序是否支持该类型。当provider提供了复杂的数据类型时,你可能需要MIME数据类型的帮忙。如,通讯录应用程序的contentprovider包含一张ContractsContract.data的表,在表中每一行,就是通过MIME类型标记联系人的数据信息,如需获得content URI中的MIME类型,可以调用ContentResolver.getType()。
4、访问Provider的其他方式
除了上面介绍的访问Content Provider的方式外,还有三种方式
1)批量访问:你可以使用ContentProviderOperation类对provider进行批量访问,并在cilent调用ContentResovler.applyBatch()进行批量访问。
当需要对表中的多行数据、或者需要对多表进行操作时,这就需要批量操作,除此之外,实现数据库的事务操作,也需要批量操作。为了实现批量模式,需要一组ContentProviderOperation对象,并在cilent端调用ContentResolver.applyBatch(),向该方法中传入provider的authority,而不是content URI,因为这可以对不同表进行操作,ContentResovler.applyBatch()方法将返回一组结果
2)异步查询:你应当在子线程执行查询操作,可以使用CursorLoader类来实现。
3)通过Intent访问数据,虽然不能直接向provider传递数据,但是可以向provider所在的应用传递,这种方式提供了修改provider数据的最齐全方式。
向应用程序发送intent,即便没有访问该应用程序中的provider权限,你依然可以访问provider的数据,甚至通过返回的intent还能获得URI权限,你可以设置flag获取intent中的权限:
读取权限:FLAG_GRANT_READ_URI_PERMISSION
写入权限:FLAG_GRANT_WRITE_URI_PERMISSION
provider在清单文件中通过android:grantUriPermission属性、以及provider的子标签
下面的例子演示了如何从通讯录应用中的provider管理表中获取联系人信息(无需声明READ_CONTRACT权限)
1)调用StartActivityForResult()方法,传入intent,intent应包含ACTION_PICK的action,和contract的MIME类型CONTENT_ITEM_TYPE
2)这会启动通讯录应用的selectionActivity,该activity会切至前台
3)在selection activity中,用户会选择某个联系人并修改其信息,此时,setResult(resultCode,intent)方法将被回调,并将intent回传至你的应用中,intent包含了用户选择的联系人content URI,并在extra中包含了FLAG_GRANT_READ_URI_PERMISSON的flag,该flag提供了你的应用程序访问联系人中指定数据的权限,最后,selectionactivity调用finish()destory
4)你的activity切回前台并回调onActivityResult()方法获得回传的intent,从intent中获取的content URI可以直接访问联系人应用中的provider数据而无需在清单文件中声明权限
5、MIME类型引用
MIME类型使用
provider可以返回标准或定制的MIME类型字符串
MIME类型的标准形式:
type/subtype
比如,常用的MIME类型text/html,表示其指向的数据包含text类型和html类型,若provider返回的uri中包含的MIME类型为text/html,那么该返回结果将包含html标签。
定制的MIME类型,也称作vendor-specific类型,他具有更复杂的结构
其中type类型中:
vnd.android.cursor.dir表示uri指向的数据包含多行,而vnd.android.cursor.item表示包含一行。
而subtype
时provider定制的(provider-specific),系统自带的provider通常包含一个简单的subtype
,如通讯录应用中增加一行电话号码的记录,那么指向它的URI中的MIME类型将为:
vnd.android.cursor.item/phone_v2