本文主要讲解在Android开发中ContentProvider的常规用法,仅供学习分享使用,若有不足之处,还请指正。android
在Android开发中,应用程序经过ContentResolver(内容解析器)从ContentProvider(内容提供者)中获取数据,ContentResolver提供访问ContentProvider中同名方法,ContentProvider包括ContentProvider和它的子类,ContentResolver对ContentProvider的持久层存储提供了基本的CRUD(Create,Retrieve,Update,Delete)方法进行访问。客户端App的ContentResolver对象自动处理和ContentProvider的App之间的进程间通讯。ContentProvider还充当数据库和外部数据视图表现之间的抽象层。sql
备注:若是要访问一个ContentProvider,App须要在清单文件中请求对应的权限。数据库
例如:从User Dictionary Provider中获取单词和区域的列表,能够调用ContentResolver.query()方法,以下图所示:数组
1 // 查询用户定义字典并返回结果 2 mCursor = getContentResolver().query( 3 UserDictionary.Words.CONTENT_URI, // 单词表的内容URI 4 mProjection, // 查询的数据列名数组 5 mSelectionClause //查询条件,能够为null 6 mSelectionArgs, // 查询参数,能够为null 7 mSortOrder); // 返回数据对象的排序条件
下表显示了query(Uri,projection,selection,selectionArgs,sortOrder) 如何与SQL语句进行匹配:安全
Content URI是Provider中标识数据的URI,包括整个Provider(其权限)的符号名和指向表(或路径)的名称,Content URI是访问ContentProvider的参数之一。app
在前面的代码行中,常量_uri包含了用户词典“Word”表的Content URI。ContentResolver对象经过将权限与已知提供者的系统表进行比较,将查询参数发送到正确的Provider。 异步
ContentProvider使用URI的路径部分来选择要访问的表,一般为公开的每一个表设置路径。ide
在前面的代码行中,“Word”表的全称为:函数
1 content://user_dictionary/words
其中user_dictionary 字符串是Provider的权限,而 words是表的路径。content:// (the scheme)始终存在,并将其标识为Content URI。布局
许多Provider容许将id值附加到URI的末尾来访问表中的单个行。例如,要从User Dictionary中检索_id为4的行,可使用Content URI:
1 Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
当要修改或删除其中一行时,常用ID值。
备注:Uri 和 Uri.Builder类包含了用字符串构造形式良好的uri对象的方法。ContentUris包含了将ID附加到uri的方法。前面片断使用withAppendedId() 将ID附加到UserDictionary.Words.CONTENT_URI。
本节介绍如何使用User Dictionary Provider做为示例,从中检索数据。
为了清晰起见,本节中的代码段调用“UI线程”上的ContentResolver.query()。在实际代码中,应该在非UI线程上异步地进行查询。
要从Provider获取数据,请遵循如下基本步骤:
要从Provider中检索数据,应用程序须要Provider的“读取访问权限”。不能在运行时请求此权限;必须在您的清单中指定须要此权限,使用<uses-permission>元素和由Provider定义的权限名称。当在清单中指定此元素时,其实是在为App“请求”此权限。当用户安装App时,会隐式地批准这个请求。
User Dictionary Provider在清单文件中定义的权限名称为android.permission.READ_USER_DICTIONARY,因此App中想要从Provider中获取数据,须要请求这个权限。
查询数据的下一步是构造查询。如下片断定义了访问User Dictionary Provider的一些变量:
1 // "projection" 定义每行返回的列名数组 2 String[] mProjection = 3 { 4 UserDictionary.Words._ID, // _ID column name 5 UserDictionary.Words.WORD, // word column name 6 UserDictionary.Words.LOCALE // locale column name 7 }; 8 9 // 定义查询条件 10 String mSelectionClause = null; 11 12 // 定义查询条件参数 13 String[] mSelectionArgs = {""};
下一个片断显示如何使用ContentResolver.query(),以User Dictionary Provider 为例,查询相似于sql查询,它包含要返回的列名、查询条件和排序。
查询返回的列集合称为投影(变量投影)。
查询条件表达式被拆分为选择子句和选择参数。选择子句是逻辑表达式和布尔表达式、列名称和值的组合。若是指定可替换参数“?”,查询条件再也不是一个值,而是从条件参数数组(mSelectionArgs)中查询该值。
若是用户没有输入一个单词,则选择子句设置为空,查询返回Provider中的全部单词。
若是用户输入了一个单词,查询条件将设置UserDictionary.Words.WORD + " = ?"。参数数组的第一个元素设置为用户输入的单词。
1 /* 2 * 定义查询参数 3 */ 4 String[] mSelectionArgs = {""}; 5 6 // 获取界面输入的查询条件 7 mSearchString = mSearchWord.getText().toString(); 8 9 //此处插入代码校验数据是否有效 10 //若是条件为空,则查询全部 11 if (TextUtils.isEmpty(mSearchString)) { 12 // 若是查询条件为空,则返回全部内容 13 mSelectionClause = null; 14 mSelectionArgs[0] = ""; 15 } else { 16 // 构造查询条件,匹配用户输入的数据. 17 mSelectionClause = UserDictionary.Words.WORD + " = ?"; 18 // 查询参数. 19 mSelectionArgs[0] = mSearchString; 20 } 21 22 // 对表进行查询并返回Cursor对象 23 mCursor = getContentResolver().query( 24 UserDictionary.Words.CONTENT_URI, // URI 25 mProjection, // 查询数据列 26 mSelectionClause // 查询条件 27 mSelectionArgs, // 查询参数 28 mSortOrder); // 返回结果排序行 29 30 // 若是出现查询异常,则返回空 31 if (null == mCursor) { 32 /* 33 * 插入代码捕获异常 34 */ 35 // 若是返回为空,则没有匹配的内容 36 } else if (mCursor.getCount() < 1) { 37 /* 38 * 通知用户查询不成功. 但这不是必须的*/ 39 } else { 40 // 插入代码处理结果 41 }
相似于sql语句:
1 SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
在这个sql语句中,使用的是实际的列名称,而不是Contract类常量。
若是content provider管理的数据在sql数据库中,外部不受信任的数据输入到原始sql语句中,就会致使sql注入。
考虑这个查询条件:
1 //经过拼接用户输入和列名的方式构造查询条件 2 String mSelectionClause = "var = " + mUserInput;
若是这样作,用户可能将恶意sql链接到您的sql语句中。例如,用户能够输入"nothing; DROP TABLE *;"用于mUserInput,这将致使选择子句var = nothing; DROP TABLE *;。因为选择条件被视为sql语句,这可能会致使Provider删除sqlite数据库中的全部表。
为了不此问题,请使用可替换的参数和单独的选择参数数组的查询条件。采用这种方式,用户输入将直接绑定到查询,而不是被解释为sql语句的一部分,用户没法注入恶意sql。以下所示:
1 // 用一个可替换参数来包含用户输入 2 String mSelectionClause = "var = ?";
以下设置查询参数数组:
1 // 定义一个查询条件的数组 2 String[] selectionArgs = {""};
在查询参数数组中进行赋值:
1 // 将用户数据做为参数数据 2 selectionArgs[0] = mUserInput;
ContentResolver.query()客户端方法老是返回一个Cursor。Cursor对象提供对其包含的行和列的读取访问权。使用Cursor中的方法能够迭代行数据,肯定每列的数据类型,将数据从列中取出,并检查结果的其余属性。有些Cursor实现会在提供者的数据变动时自动更新,或在Cursor变动时触发对应的事件,或二者兼而有之。
若是没有行符合查询条件,provider将返回一个Cursor, 其Cursor.getCount()为0(空光标)。
若是发生内部错误,查询的结果取决于特定的Provider。它能够返回null,也能够抛出异常。
因为Cursor是行的“列表”,显示Cursor内容的一个好方法是经过SimpleCursorAdapter绑定到ListView。
以下代码所示:它建立一个SimpleCursorAdapter对象,包含查询到的Cursor,并将此对象设置到ListView的适配器
1 // 定义从Cursor中检索并加载到输出行的列名 2 String[] mWordListColumns = 3 { 4 UserDictionary.Words.WORD, // word column name 5 UserDictionary.Words.LOCALE // locale column name 6 }; 7 8 //定义一个视图ID列表,该列表将接收每行的Cursor列 9 int[] mWordListItems = { R.id.dictWord, R.id.locale}; 10 11 // 建立一个SimpleCursorAdapter对象 12 mCursorAdapter = new SimpleCursorAdapter( 13 getApplicationContext(), // 应用程序上下文对象 14 R.layout.wordlistrow, // ListView单行配置文件 15 mCursor, // query函数返回的结果 16 mWordListColumns, // Cursor中的列名数组 17 mWordListItems, // ListView中Item项的布局文件 18 0); // Flags (usually none are needed) 19 20 // 设置 adapter到ListView 21 mWordList.setAdapter(mCursorAdapter);
备注:要使用Cursor支持ListView,Cursor必须包含一个名为_id的列。这个限制也解释了为何大多数Provider的每一个表都有一个_id列。
您能够将查询结果用于其余任务,而不是简单地显示查询结果。要作到这一点,须要迭代Cursor中的行:
1 // 定义"word"列的索引 2 int index = mCursor.getColumnIndex(UserDictionary.Words.WORD); 3 4 /* 5 * 当cursor有效的时候才执行. User Dictionary Provider若是发生内部错误,将返回null,其余provider可能会抛出异常 6 */ 7 8 if (mCursor != null) { 9 /* 10 * 移动到cursor的下一行.在第一行移动以前, 行指向是-1,若是试图去查询此位置上的内容,将会抛出一个异常 11 */ 12 while (mCursor.moveToNext()) { 13 //获取对应的列的值. 14 newWord = mCursor.getString(index); 15 // 插入代码处理获取的值. 16 ... 17 // while 循环结束 18 } 19 } else { 20 // 展现错误和异常信息 21 }
Cursor实现包含检索不一样类型数据的几种“get”方法。例如,上一个片断使用getString()。同时也有一个gettype()方法,该方法返回列的数据类型。
访问Provider中的数据,调用方必须具备相应的权限,这些权限确保用户知道应用程序试图访问哪些数据,用户在安装App时会看到请求的权限。
如前所述,User Dictionary Provider要求使用android.permission.READ_USER_DICTIONARY权限获取数据。Provider须要android.permission.WRITE_USER_DICTIONARY权限来插入、更新或删除数据。
为了得到访问provider所需的权限,App在其清单文件中以<uses-permission>元素请求它们。当安装App时,用户必须容许应用程序请求的全部权限。若是用户所有容许,将继续安装;若是用户不容许,Package Manager将停止安装。
如下<uses-permission>元素请求读取 User Dictionary Provider的访问权限:
1 <uses-permission android:name="android.permission.READ_USER_DICTIONARY">
与从provider获取数据的方式相同,还可使用provider客户端与provider's 提供方之间的交互来修改数据。provider 和provider客户端自动处理安全以及进程间通讯。
将数据插入到provider中,请调用ContentResolver.insert()。此方法将新行插入到provider中,并返回新增行的 content URI。此片断显示如何将新词插入到User Dictionary Provider中:
1 // 定义一个新的 Uri对象,接收插入新行放回的内容 2 Uri mNewUri; 3 4 // 要插入的新值 5 ContentValues mNewValues = new ContentValues(); 6 7 /* 8 * 设置每列对应的值 9 */ 10 mNewValues.put(UserDictionary.Words.APP_ID, "example.user"); 11 mNewValues.put(UserDictionary.Words.LOCALE, "en_US"); 12 mNewValues.put(UserDictionary.Words.WORD, "insert"); 13 mNewValues.put(UserDictionary.Words.FREQUENCY, "100"); 14 15 mNewUri = getContentResolver().insert( 16 UserDictionary.Word.CONTENT_URI, // 内容 URI 17 mNewValues // 插入的值 18 );
新行的数据对应单个ContentValues对象,该对象在形式上相似于单行cursor。此对象中的列不须要具备相同的数据类型,若是不想指定值,则可使用ContentValues.putNull()设置列为空。
代码段不会添加_id列,由于此列是自动维护的。provider为添加的每一行指定一个惟一_id,一般使用_id做为表的主键。
返回的新行的newUri,格式以下:
1 content://user_dictionary/words/<id_value>
<id_value>是新行的_id。大多数provider能够自动检测到这种形式的内容,而后在该特定行上执行请求的操做。
若要从返回的Uri中获得_id值,请调用ContentUris.parseId()。
要更新行,将使用带有更新值的ContentValues对象,就像使用插入时同样,选择条件也与使用查询时同样。调用方法是ContentResolver.update()。您只须要为须要更新的列向ContentValues对象添加值。若是要清除列的内容,请将值设置为null。
下面的片断将locale设置有语言"en"的全部行更改成locale为空。返回值是更新的行数:
1 // 包含更新的内容的对象 2 ContentValues mUpdateValues = new ContentValues(); 3 4 // 定义须要更新的查询条件 5 String mSelectionClause = UserDictionary.Words.LOCALE + "LIKE ?"; 6 String[] mSelectionArgs = {"en_%"}; 7 8 // 定义更新行获得的行数 9 int mRowsUpdated = 0; 10 11 /* 12 * 设置更新的内容. 13 */ 14 mUpdateValues.putNull(UserDictionary.Words.LOCALE); 15 16 mRowsUpdated = getContentResolver().update( 17 UserDictionary.Words.CONTENT_URI, // URI 18 mUpdateValues // 更新的内容 19 mSelectionClause //查询条件 20 mSelectionArgs // 查询内容参数 21 );
在调用ContentResolver.update()时,对用户输入进行处理。
删除行相似于查询行数据:为要删除的行指定选择条件,而客户端方法返回已删除行的数目以下所示:
1 // 定义须要删除的条件 2 String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?"; 3 String[] mSelectionArgs = {"user"}; 4 5 //定义删除掉行数 6 int mRowsDeleted = 0; 7 8 // 删除匹配条件的内容 9 mRowsDeleted = getContentResolver().delete( 10 UserDictionary.Words.CONTENT_URI, // URI 11 mSelectionClause // 删除条件 12 mSelectionArgs // 删除参数 13 );
在调用 ContentResolver.delete()方法时,对用户输入进行处理。
Content providers能够提供许多不一样的数据类型。User Dictionary Provider只提供文本,但也能够提供如下格式:
providers常用的另外一种数据类型是Binary Large OBject (BLOB),它是64kb字节数组。经过查看Cursor类“get”方法,能够看到可用的数据类型。
provider中每一列的数据类型一般在其文档中列出。User Dictionary Provider 的数据类型在其contract类UserDictionary.Words的参考文档中列出。也能够经过Cursor.getType()来肯定数据类型。
在应用程序开发中,三种可供选择的Provider访问形式很是重要:
对provider的批量访问用于插入多行,或在同一方法中在多个表中插入行,或一般用于做为事务(原子操做)执行一组跨进程的操做。
要以“batch mode”访问provider,您能够建立一组 ContentProviderOperation 对象,而后经过ContentResolver.applyBatch()方法将对象分发到provider。将provider的权限传递给此方法,而不是特定的内容。这容许数组中的每一个ContentProviderOperation对象对不一样的表操做。ContentResolver.applyBatch() 返回结果数组。
Intents能够提供对 content provider的间接访问。容许用户访问provider中的数据,即便您的App没有访问权限,也能够从有权限的App得到结果Intent,或者经过激活有权限的App并在其中工做。
contract类定义了帮助App处理content URIs、列名称、意图操做和 content provider的其余特性的常量。Contract类不自动包含在provider中;provider的开发人员必须定义它们,而后将其提供给其余开发人员。android平台中的许多提供商在android.provider中都有相应的contract类。
例如,User Dictionary Provider有一个包含内容URI和列名常量的contract类用户词典。“单词”表的内容以“常量”为定义。UserDictionary.Words.CONTENT_URI,在如下示例片断中使用。例如,查询投影能够定义为:
1 String[] mProjection = 2 { 3 UserDictionary.Words._ID, 4 UserDictionary.Words.WORD, 5 UserDictionary.Words.LOCALE 6 };
读取通话记录
1 //通信记录URI 2 private String call_uri = "content://call_log/calls"; 3 4 //内容解析器 5 private ContentResolver mResolver; 6 7 //列表 8 private ListView lvCall; 9 10 //获取的通信记录的列名 11 private String[] columns = new String[]{ 12 CallLog.Calls._ID, CallLog.Calls.CACHED_NAME, CallLog.Calls.NUMBER, CallLog.Calls.TYPE, CallLog.Calls.DATE,CallLog.Calls.DURATION 13 }; 14 15 @Override 16 protected void onCreate(Bundle savedInstanceState) { 17 super.onCreate(savedInstanceState); 18 setContentView(R.layout.activity_main); 19 //初始化内容解析器 20 mResolver = getContentResolver(); 21 lvCall = (ListView) this.findViewById(R.id.lv_call); 22 } 23 24 /** 25 * 获取通信记录事件 26 * @param v 27 */ 28 public void bn_call(View v) { 29 List<Map<String, String>> list = new ArrayList<>(); 30 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 31 Cursor cursor = mResolver.query(Uri.parse(call_uri), columns, null, null, CallLog.Calls.DEFAULT_SORT_ORDER); 32 //如下是为了转换数据格式 33 if(cursor!=null){ 34 while (cursor.moveToNext()){ 35 long dt=cursor.getLong(cursor.getColumnIndex("date")); 36 Date callDate = new Date(dt); 37 String callDateStr = sdf.format(callDate); 38 String name=cursor.getString(cursor.getColumnIndex("name")); 39 String number=cursor.getString(cursor.getColumnIndex("number")); 40 String duration =cursor.getString(cursor.getColumnIndex("duration"))+"s"; 41 Map<String, String> map=new HashMap<String, String>() ; 42 map.put("name",name); 43 map.put("number",number); 44 map.put("date",callDateStr); 45 map.put("duration",duration); 46 list.add(map); 47 } 48 } 49 //将数据填充到Adapter 50 SimpleAdapter adapter=new SimpleAdapter(this,list,R.layout.list_item, 51 new String[]{"name", "number", "date","duration"}, 52 new int[]{R.id.tv_name, R.id.tv_number, R.id.tv_time,R.id.tv_duration}); 53 54 /*SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.list_item, cursor, 55 new String[]{"name", "number", "date"}, 56 new int[]{R.id.tv_name, R.id.tv_number, R.id.tv_time}, 57 CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);*/ 58 //绑定Adapter到ListView 59 lvCall.setAdapter(adapter); 60 }
读取短信记录
1 private String sms_uri="content://sms"; 2 3 private String[] columns=new String[]{ 4 Telephony.Sms._ID, Telephony.Sms.ADDRESS,Telephony.Sms.CREATOR, Telephony.Sms.BODY, Telephony.Sms.DATE, Telephony.Sms.PERSON, Telephony.Sms.STATUS, Telephony.Sms.DATE_SENT 5 }; 6 7 private ContentResolver mResolver; 8 9 private ListView lvMsg; 10 11 @Override 12 protected void onCreate(Bundle savedInstanceState) { 13 super.onCreate(savedInstanceState); 14 setContentView(R.layout.activity_main2); 15 mResolver=getContentResolver(); 16 lvMsg= (ListView) this.findViewById(R.id.lv_sms); 17 } 18 19 public void bn_sms(View view) { 20 List<Map<String, String>> list = new ArrayList<>(); 21 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); 22 Cursor cursor = mResolver.query(Uri.parse(sms_uri), columns, null, null, Telephony.Sms.DEFAULT_SORT_ORDER); 23 if (cursor != null) { 24 while (cursor.moveToNext()) { 25 Log.e("TAG", "bn_sms: "+cursor.getColumnIndex("person")+"---"+ cursor.getColumnIndex("date")+"---"+cursor.getColumnIndex("creator")+"---"+cursor.getColumnIndex("address")); 26 long dt = cursor.getLong(cursor.getColumnIndex("date")); 27 Date callDate = new Date(dt); 28 String callDateStr = sdf.format(callDate); 29 String person = cursor.getString(cursor.getColumnIndex("address")); 30 String creator = cursor.getString(cursor.getColumnIndex("creator")); 31 //String duration =cursor.getString(cursor.getColumnIndex("duration"))+"s"; 32 String body = cursor.getString(cursor.getColumnIndex("body")); 33 Map<String, String> map = new HashMap<String, String>(); 34 map.put("person", person); 35 map.put("creator", creator); 36 map.put("date", callDateStr); 37 //map.put("duration",duration); 38 map.put("body", body); 39 list.add(map); 40 } 41 } 42 //将数据填充到Adapter 43 SimpleAdapter adapter = new SimpleAdapter(this, list, R.layout.msg_item, 44 new String[]{"person", "creator", "date", "body"}, 45 new int[]{R.id.tv_name, R.id.tv_number, R.id.tv_time, R.id.tv_msg}); 46 47 //绑定Adapter到ListView 48 lvMsg.setAdapter(adapter); 49 }
千里之行,始于足下。