当前位置: 移动技术网 > IT编程>移动开发>Android > 一起学Android之ContentProvider

一起学Android之ContentProvider

2019年06月19日  | 移动技术网IT编程  | 我要评论

骆嘉琦,蜗牛公司,马六甲生态板

本文主要讲解在android开发中contentprovider的常规用法,仅供学习分享使用,如有不足之处,还请指正。

访问一个contentprovider

android开发中,应用程序通过contentresolver(内容解析器)contentprovider(内容提供者)中获取数据,contentresolver提供访问contentprovider中同名方法,contentprovider包括contentprovider和它的子类,contentresolvercontentprovider的持久层存储提供了基本的crudcreate,retrieve,update,delete)方法进行访问。客户端appcontentresolver对象自动处理和contentproviderapp之间的进程间通信。contentprovider还充当数据库和外部数据视图表现之间的抽象层。

备注:如果要访问一个contentproviderapp需要在清单文件中请求对应的权限。

例如:从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 uris

content uriprovider中标识数据的uri,包括整个provider(其权限)的符号名和指向表(或路径)的名称,content uri是访问contentprovider的参数之一。

在前面的代码行中,常量_uri包含了用户词典“word”表的content uricontentresolver对象通过将权限与已知提供者的系统表进行比较,将查询参数发送到正确的provider 

contentprovider使用uri的路径部分来选择要访问的表,通常为公开的每个表设置路径。

在前面的代码行中,word”表的全称为:

1 content://user_dictionary/words

其中user_dictionary 字符串是provider的权限,words是表的路径。content:// (the scheme)始终存在,并将其标识为content uri

许多provider允许将id值附加到uri的末尾来访问表中的单个行。例如,要从user dictionary中检索_id4的行,可以使用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

provider获取数据

本节介绍如何使用user dictionary provider作为示例,从中检索数据。

为了清晰起见,本节中的代码段调用ui线程”上的contentresolver.query()。在实际代码中,应该在非ui线程上异步地进行查询。

要从provider获取数据,请遵循以下基本步骤:

  1. 请求读取provider的访问权限。
  2. 定义查询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()客户端方法总是返回一个cursorcursor对象提供对其包含的行和列的读取访问权。使用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支持listviewcursor必须包含一个名为_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()方法,该方法返回列的数据类型。

content provider权限

访问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">

inserting, updating, and deleting data

与从provider获取数据的方式相同,还可以使用provider客户端与provider's 提供方之间的交互来修改数据。provider provider客户端自动处理安全以及进程间通信。

插入数据(inserting data

将数据插入到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()

更新数据(updating data

要更新行,将使用带有更新值的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()时,对用户输入进行处理。

删除数据(deleting data

删除行类似于查询行数据:为要删除的行指定选择条件,而客户端方法返回已删除行的数目如下所示:

 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()方法时,对用户输入进行处理。

provider数据类型

content providers可以提供许多不同的数据类型。user dictionary provider只提供文本,但也可以提供以下格式:

  •  integer
  •  long integer (long)
  •  floating point
  •  long floating point (double)

providers经常使用的另一种数据类型是binary large object (blob),它是64kb字节数组。通过查看cursor类“get”方法,可以看到可用的数据类型。

provider中每一列的数据类型通常在其文档中列出。user dictionary provider 的数据类型在其contractuserdictionary.words的参考文档中列出。也可以通过cursor.gettype()来确定数据类型。

provider访问的替代形式

在应用程序开发中,三种可供选择的provider访问形式非常重要:

  1. 批量访问:可以在contentprovideroperation中使用方法创建批量处理访问调用,然后用contentresolver.applybatch()应用它们。
  2. 异步查询:应该在单独的线程中进行查询,其中一种方法是使用cursorloader对象。
  3. 通过intents访问数据:虽然不能直接向提供者发送intent,但可以向provider's application发送intent,而provider's application序通常是最适合修改provider数据的应用程序。

批量访问(batch access

provider的批量访问用于插入多行,或在同一方法中在多个表中插入行,或通常用于作为事务(原子操作)执行一组跨进程的操作。

要以batch mode”访问provider,您可以创建一组 contentprovideroperation 对象,然后通过contentresolver.applybatch()方法将对象分发到provider。将provider的权限传递给此方法,而不是特定的内容。这允许数组中的每个contentprovideroperation对象对不同的表操作。contentresolver.applybatch() 返回结果数组。

通过intent进行数据访问(data access via intents

intents可以提供对 content provider的间接访问。允许用户访问provider中的数据,即使您的app没有访问权限,也可以从有权限的app获得结果intent,或者通过激活有权限的app并在其中工作。

合同类别(contract classes

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 };

 contentprovider示例

读取通话记录

 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     }

备注

千里之行,始于足下。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网