`

Android应用程序组件Content Provider应用实例

 
阅读更多

上文简要介绍了Android应用程序组件Content Provider在应用程序间共享数据的原理,但是没有进一步研究它的实现。本文将实现两个应用程序,其中一个以Content Provider的形式来提供数据访问入口,另一个通过这个Content Provider来访问这些数据。本文的例子不仅可以为下文分析Content Provider的实现原理准备好使用情景,还可以学习到它的一个未公开接口。

本文中的应用程序是按照上一篇文章Android应用程序组件Content Provider简要介绍和学习计划中提到的一般应用程序架构方法来设计的。本文包含两个应用程序,其中,第一个应用程序命名为ArticlesProvider,它使用了SQLite数据库来维护一个文章信息列表,同时,它定义了访问这个文章信息列表的URI,这样,我们就可以通过一个Content Provider组件来向第三方应用程序提供访问这个文章信息列表的接口;第二个应用程序命名为Article,它提供了管理保存在ArticlesProvider应用程序中的文章信息的界面入口,在这个应用程序中,用户可以添加、删除和修改这些文章信息。接下来我们就分别介绍这两个应用程序的实现。

1. ArticlesProvider应用程序的实现

首先是参照在Ubuntu上为Android系统内置Java应用程序测试Application Frameworks层的硬件服务一文,在packages/experimental目录下建立工程文件目录ArticlesProvider。在继续介绍这个应用程序的实现之前,我们先介绍一下这个应用程序用来保存文章信息的数据库的设计。

我们知道,在Android系统中,内置了一款轻型的数据库SQLite。SQLite是专门为嵌入式产品而设计的,它具有占用资源低的特点,而且是开源的,非常适合在Android平台中使用,关于SQLite的更多信息可以访问官方网站http://www.sqlite.org。

ArticlesProvider应用程序就是使用SQLite来作为数据库保存文章信息的,数据库文件命名为Articles.db,它里面只有一张表ArticlesTable,表的结构如下所示:

-------------------------------------------------------------

| --_id --| -- _title -- | --_abstrat --| -- _url --|

-------------------------------------------------------------

| | | | |

它由四个字段表示,第一个字段_id表示文章的ID,类型为自动递增的integer,它作为表的key值;第二个字段_title表示文章的题目,类型为text;第三个字段_abstract表示文章的摘要,类型为text;第四个字段_url表示文章的URL,类型为text。注意,当我们打算将数据库表的某一列的数据作为一个数据行的ID时,就约定它的列名为_id。这是因为我们经常需要从数据库中获取一批数据,这些数据以Cursor的形式返回,对这些返回来的数据我们一般用一个ListView来显示,而这个ListView需要一个数据适配器Adapter来作为数据源,这时候就我们就可以以这个Cursor来构造一个Adapter。有些Adapter,例如android.widget.CursorAdapter,它们在实现自己的getItemId成员函数来获取指定数据行的ID时,就必须要从这个Cursor中相应的行里面取出列名为_id的字段的内容出来作为这个数据行的ID返回给调用者。当然,我们不在数据库表中定义这个_id列名也是可以的,不过这样从数据库中查询数据后得到的Cursor适合性就变差了,因此,建议我们在设计数据库表时,尽量设置其中一个列名字_id,并且保证这一列的内容是在数据库表中是唯一的。

下面我们就开始介绍这个应用程序的实现了。这个应用程序只有两个源文件,分别是Articles.java和ArticlesProvider,都是放在shy.luo.providers.articles这个package下面。在Articles.java文件里面,主要是定义了一些常量,例如用来访问文章信息数据的URI、MIME(Multipurpose Internet Mail Extensions)类型以及格式等,这些常量是第三方应用程序访问这些文章信息数据时要使用到的,因此,我们把它定义在一个单独的文件中,稍后我们会介绍如果把这个Articles.java文件打包成一个jar文件,然后第三方应用程序就可以引用这个常量了,这样也避免了直接把这个源代码文件暴露给第三方应用程序。

源文件Articles.java位于src/shy/luo/providers/articles目录下,它的内容如下所示:

view plain
  1. packageshy.luo.providers.articles;
  2. importandroid.net.Uri;
  3. publicclassArticles{
  4. /*DataField*/
  5. publicstaticfinalStringID="_id";
  6. publicstaticfinalStringTITLE="_title";
  7. publicstaticfinalStringABSTRACT="_abstract";
  8. publicstaticfinalStringURL="_url";
  9. /*Defaultsortorder*/
  10. publicstaticfinalStringDEFAULT_SORT_ORDER="_idasc";
  11. /*CallMethod*/
  12. publicstaticfinalStringMETHOD_GET_ITEM_COUNT="METHOD_GET_ITEM_COUNT";
  13. publicstaticfinalStringKEY_ITEM_COUNT="KEY_ITEM_COUNT";
  14. /*Authority*/
  15. publicstaticfinalStringAUTHORITY="shy.luo.providers.articles";
  16. /*MatchCode*/
  17. publicstaticfinalintITEM=1;
  18. publicstaticfinalintITEM_ID=2;
  19. publicstaticfinalintITEM_POS=3;
  20. /*MIME*/
  21. publicstaticfinalStringCONTENT_TYPE="vnd.android.cursor.dir/vnd.shy.luo.article";
  22. publicstaticfinalStringCONTENT_ITEM_TYPE="vnd.android.cursor.item/vnd.shy.luo.article";
  23. /*ContentURI*/
  24. publicstaticfinalUriCONTENT_URI=Uri.parse("content://"+AUTHORITY+"/item");
  25. publicstaticfinalUriCONTENT_POS_URI=Uri.parse("content://"+AUTHORITY+"/pos");
  26. }
ID、TITLE、ABSTRACT和URL四个常量前面已经解释过了,它是我们用来保存文章信息的数据表的四个列名;DEFAULT_SORT_ORDER常量是调用ContentProvider接口的query函数来查询数据时用的,它表示对查询结果按照_id列的值从小到大排列;METHOD_GET_ITEM_COUNT和KEY_ITEM_COUNT两个常量是调用ContentProvider接口的一个未公开函数call来查询数据时用的,它类似于微软COM中的IDispatch接口的Invoke函数,使用这个call函数时,传入参数METHOD_GET_ITEM_COUNT表示我们要调用我们自定义的ContentProvider子类中的getItemCount函数来获取数据库中的文章信息条目的数量,结果放在一个Bundle中以KEY_ITEM_COUNT为关键字的域中。

剩下的常量都是跟数据URI相关的,这个需要详细解释一下。URI的全称是Universal Resource Identifier,即通用资源标志符,通过它用来唯一标志某个资源在网络中的位置,它的结构和我们常见的HTTP形式URL是一样的,其实我们可以把常见的HTTP形式的URL看成是URI结构的一个实例,URI是在更高一个层次上的抽象。在Android系统中,它也定义了自己的用来定痊某个特定的Content Provider的URI结构,它通常由四个组件来组成,如下所示:

[content://][shy.luo.providers.articles][/item][/123]

|------A------|-----------------B-------------------|---C---|---D--|

A组件称为Scheme,它固定为content://,表示它后面的路径所表示的资源是由Content Provider来提供的。

B组件称为Authority,它唯一地标识了一个特定的Content Provider,因此,这部分内容一般使用Content Provider所在的package来命名,使得它是唯一的。

C组件称为资源路径,它表示所请求的资源的类型,这部分内容是可选的。如果我们自己所实现的Content Provider只提供一种类型的资源访问,那么这部分内部就可以忽略;如果我们自己实现的Content Provider同时提供了多种类型的资源访问,那么这部分内容就不可以忽略了。例如,我们有两种电脑资源可以提供给用户访问,一种是笔记本电脑,一种是平板电脑,我们就把分别它们定义为notebook和pad;如果我们想进一步按照系统类型来进一步细分这两种电脑资源,对笔记本电脑来说,一种是安装了windows系统的,一种是安装了linux系统的,我们就分别把它们定义为notebook/windows和notebook/linux;对平板电脑来说,一种是安装了ios系统的,一种是安装了android系统的,我们就分别把它们定义为pad/ios和pad/android。

D组件称为资源ID,它表示所请求的是一个特定的资源,它通常是一个数字,对应前面我们所介绍的数据库表中的_id字段的内容,它唯一地标志了某一种资源下的一个特定的实例。继续以前面的电脑资源为例,如果我们请求的是编号为123的装了android系统的平板电脑,我们就把它定义为pad/android/123。当忽略这部分内容时,它有可能是表示请求某一种资源下的所有实例,取决于我们的URI匹配规则,后面我们将会进一步解释如何设置URI匹配规则。

回到上面的Articles.java源文件中,我们定义了两个URI,分别用COTENT_URI和CONTENT_POS_URI两个常量来表示,它们的Authority组件均指定为shy.luo.providers.articles。其中,COTENT_URI常量表示的URI表示是通过ID来访问文章信息的,而CONTENT_POS_URI常量表示的URI表示是通过位置来访问文章信息的。例如,content://shy.luo.providers.articles/item表示访问所有的文章信息条目;content://shy.luo.providers.articles/item/123表示只访问ID值为123的文章信息条目;content://shy.luo.providers.articles/pos/1表示访问数据库表中的第1条文章信息条目,这条文章信息条目的ID值不一定为1。通过常量CONTENT_POS_URI来访问文章信息条目时,必须要指定位置,这也是我们设置的URI匹配规则来指定的,后面我们将会看到。

此外,我们还需要定义与URI对应的资源的MIME类型。每个MIME类型由两部分组成,前面是数据的大类别,后面定义具体的种类。在Content Provider中,URI所对应的资源的MIME类型的大类别根据同时访问的资源的数量分为两种,对于访问单个资源的URI,它的大类别就为vnd.android.cursor.item,而对于同时访问多个资源的URI,它的大类别就为vnd.android.cursor.dir。Content Provider的URI所对应的资源的MIME类型的具体类别就需要由Content Provider的提供者来设置了,它的格式一般为vnd.[company name].[resource type]的形式。例如,在我们的例子中,CONTENT_TYPE和COTENT_ITEM_TYPE两个常量分别定义了两种MIME类型,它们的大类别分别为vnd.android.cursor.dir和vnd.android.cursor.item,而具体类别均为vdn.shy.luo.article,其中shy.luo就是表示公司名了,而article表示资源的类型为文章。这两个MIME类型常量主要是在实现ContentProvider的getType函数时用到的,后面我们将会看到。

最后,ITEM、ITEM_ID和POS_ID三个常量分别定了三个URI匹配规则的匹配码。如果URI的形式为content://shy.luo.providers.articles/item,则匹配规则返回的匹配码为ITEM;如果URI的形式为content://shy.luo.providers.articles/item/#,其中#表示任意一个数字,则匹配规则返回的匹配码为ITEM_ID;如果URI的形式为#也是表示任意一个数字,则匹配规则返回的匹配码为ITEM_POS。这三个常量的用法我们在后面也将会看到。

这样,Articles.java文件的内容就介绍完了。下面我们再接着介绍位于src/shy/luo/providers/articles目录下的ArticlesProvider.java文件,它的内容如下所示:

view plain
  1. importjava.util.HashMap;
  2. importandroid.content.ContentValues;
  3. importandroid.content.Context;
  4. importandroid.content.UriMatcher;
  5. importandroid.content.ContentProvider;
  6. importandroid.content.ContentUris;
  7. importandroid.content.ContentResolver;
  8. importandroid.database.Cursor;
  9. importandroid.database.sqlite.SQLiteDatabase;
  10. importandroid.database.sqlite.SQLiteDatabase.CursorFactory;
  11. importandroid.database.sqlite.SQLiteException;
  12. importandroid.database.sqlite.SQLiteOpenHelper;
  13. importandroid.database.sqlite.SQLiteQueryBuilder;
  14. importandroid.net.Uri;
  15. importandroid.os.Bundle;
  16. importandroid.text.TextUtils;
  17. importandroid.util.Log;
  18. publicclassArticlesProviderextendsContentProvider{
  19. privatestaticfinalStringLOG_TAG="shy.luo.providers.articles.ArticlesProvider";
  20. privatestaticfinalStringDB_NAME="Articles.db";
  21. privatestaticfinalStringDB_TABLE="ArticlesTable";
  22. privatestaticfinalintDB_VERSION=1;
  23. privatestaticfinalStringDB_CREATE="createtable"+DB_TABLE+
  24. "("+Articles.ID+"integerprimarykeyautoincrement,"+
  25. Articles.TITLE+"textnotnull,"+
  26. Articles.ABSTRACT+"textnotnull,"+
  27. Articles.URL+"textnotnull);";
  28. privatestaticfinalUriMatcheruriMatcher;
  29. static{
  30. uriMatcher=newUriMatcher(UriMatcher.NO_MATCH);
  31. uriMatcher.addURI(Articles.AUTHORITY,"item",Articles.ITEM);
  32. uriMatcher.addURI(Articles.AUTHORITY,"item/#",Articles.ITEM_ID);
  33. uriMatcher.addURI(Articles.AUTHORITY,"pos/#",Articles.ITEM_POS);
  34. }
  35. privatestaticfinalHashMap<String,String>articleProjectionMap;
  36. static{
  37. articleProjectionMap=newHashMap<String,String>();
  38. articleProjectionMap.put(Articles.ID,Articles.ID);
  39. articleProjectionMap.put(Articles.TITLE,Articles.TITLE);
  40. articleProjectionMap.put(Articles.ABSTRACT,Articles.ABSTRACT);
  41. articleProjectionMap.put(Articles.URL,Articles.URL);
  42. }
  43. privateDBHelperdbHelper=null;
  44. privateContentResolverresolver=null;
  45. @Override
  46. publicbooleanonCreate(){
  47. Contextcontext=getContext();
  48. resolver=context.getContentResolver();
  49. dbHelper=newDBHelper(context,DB_NAME,null,DB_VERSION);
  50. Log.i(LOG_TAG,"ArticlesProviderCreate");
  51. returntrue;
  52. }
  53. @Override
  54. publicStringgetType(Uriuri){
  55. switch(uriMatcher.match(uri)){
  56. caseArticles.ITEM:
  57. returnArticles.CONTENT_TYPE;
  58. caseArticles.ITEM_ID:
  59. caseArticles.ITEM_POS:
  60. returnArticles.CONTENT_ITEM_TYPE;
  61. default:
  62. thrownewIllegalArgumentException("ErrorUri:"+uri);
  63. }
  64. }
  65. @Override
  66. publicUriinsert(Uriuri,ContentValuesvalues){
  67. if(uriMatcher.match(uri)!=Articles.ITEM){
  68. thrownewIllegalArgumentException("ErrorUri:"+uri);
  69. }
  70. SQLiteDatabasedb=dbHelper.getWritableDatabase();
  71. longid=db.insert(DB_TABLE,Articles.ID,values);
  72. if(id<0){
  73. thrownewSQLiteException("Unabletoinsert"+values+"for"+uri);
  74. }
  75. UrinewUri=ContentUris.withAppendedId(uri,id);
  76. resolver.notifyChange(newUri,null);
  77. returnnewUri;
  78. }
  79. @Override
  80. publicintupdate(Uriuri,ContentValuesvalues,Stringselection,String[]selectionArgs){
  81. SQLiteDatabasedb=dbHelper.getWritableDatabase();
  82. intcount=0;
  83. switch(uriMatcher.match(uri)){
  84. caseArticles.ITEM:{
  85. count=db.update(DB_TABLE,values,selection,selectionArgs);
  86. break;
  87. }
  88. caseArticles.ITEM_ID:{
  89. Stringid=uri.getPathSegments().get(1);
  90. count=db.update(DB_TABLE,values,Articles.ID+"="+id
  91. +(!TextUtils.isEmpty(selection)?"and("+selection+')':""),selectionArgs);
  92. break;
  93. }
  94. default:
  95. thrownewIllegalArgumentException("ErrorUri:"+uri);
  96. }
  97. resolver.notifyChange(uri,null);
  98. returncount;
  99. }
  100. @Override
  101. publicintdelete(Uriuri,Stringselection,String[]selectionArgs){
  102. SQLiteDatabasedb=dbHelper.getWritableDatabase();
  103. intcount=0;
  104. switch(uriMatcher.match(uri)){
  105. caseArticles.ITEM:{
  106. count=db.delete(DB_TABLE,selection,selectionArgs);
  107. break;
  108. }
  109. caseArticles.ITEM_ID:{
  110. Stringid=uri.getPathSegments().get(1);
  111. count=db.delete(DB_TABLE,Articles.ID+"="+id
  112. +(!TextUtils.isEmpty(selection)?"and("+selection+')':""),selectionArgs);
  113. break;
  114. }
  115. default:
  116. thrownewIllegalArgumentException("ErrorUri:"+uri);
  117. }
  118. resolver.notifyChange(uri,null);
  119. returncount;
  120. }
  121. @Override
  122. publicCursorquery(Uriuri,String[]projection,Stringselection,String[]selectionArgs,StringsortOrder){
  123. Log.i(LOG_TAG,"ArticlesProvider.query:"+uri);
  124. SQLiteDatabasedb=dbHelper.getReadableDatabase();
  125. SQLiteQueryBuildersqlBuilder=newSQLiteQueryBuilder();
  126. Stringlimit=null;
  127. switch(uriMatcher.match(uri)){
  128. caseArticles.ITEM:{
  129. sqlBuilder.setTables(DB_TABLE);
  130. sqlBuilder.setProjectionMap(articleProjectionMap);
  131. break;
  132. }
  133. caseArticles.ITEM_ID:{
  134. Stringid=uri.getPathSegments().get(1);
  135. sqlBuilder.setTables(DB_TABLE);
  136. sqlBuilder.setProjectionMap(articleProjectionMap);
  137. sqlBuilder.appendWhere(Articles.ID+"="+id);
  138. break;
  139. }
  140. caseArticles.ITEM_POS:{
  141. Stringpos=uri.getPathSegments().get(1);
  142. sqlBuilder.setTables(DB_TABLE);
  143. sqlBuilder.setProjectionMap(articleProjectionMap);
  144. limit=pos+",1";
  145. break;
  146. }
  147. default:
  148. thrownewIllegalArgumentException("ErrorUri:"+uri);
  149. }
  150. Cursorcursor=sqlBuilder.query(db,projection,selection,selectionArgs,null,null,TextUtils.isEmpty(sortOrder)?Articles.DEFAULT_SORT_ORDER:sortOrder,limit);
  151. cursor.setNotificationUri(resolver,uri);
  152. returncursor;
  153. }
  154. @Override
  155. publicBundlecall(Stringmethod,Stringrequest,Bundleargs){
  156. Log.i(LOG_TAG,"ArticlesProvider.call:"+method);
  157. if(method.equals(Articles.METHOD_GET_ITEM_COUNT)){
  158. returngetItemCount();
  159. }
  160. thrownewIllegalArgumentException("Errormethodcall:"+method);
  161. }
  162. privateBundlegetItemCount(){
  163. Log.i(LOG_TAG,"ArticlesProvider.getItemCount");
  164. SQLiteDatabasedb=dbHelper.getReadableDatabase();
  165. Cursorcursor=db.rawQuery("selectcount(*)from"+DB_TABLE,null);
  166. intcount=0;
  167. if(cursor.moveToFirst()){
  168. count=cursor.getInt(0);
  169. }
  170. Bundlebundle=newBundle();
  171. bundle.putInt(Articles.KEY_ITEM_COUNT,count);
  172. cursor.close();
  173. db.close();
  174. returnbundle;
  175. }
  176. privatestaticclassDBHelperextendsSQLiteOpenHelper{
  177. publicDBHelper(Contextcontext,Stringname,CursorFactoryfactory,intversion){
  178. super(context,name,factory,version);
  179. }
  180. @Override
  181. publicvoidonCreate(SQLiteDatabasedb){
  182. db.execSQL(DB_CREATE);
  183. }
  184. @Override
  185. publicvoidonUpgrade(SQLiteDatabasedb,intoldVersion,intnewVersion){
  186. db.execSQL("DROPTABLEIFEXISTS"+DB_TABLE);
  187. onCreate(db);
  188. }
  189. }
  190. }

我们在实现自己的Content Provider时,必须继承于ContentProvider类,并且实现以下六个函数:

-- onCreate(),用来执行一些初始化的工作。

-- query(Uri, String[], String, String[], String),用来返回数据给调用者。

-- insert(Uri, ContentValues),用来插入新的数据。

-- update(Uri, ContentValues, String, String[]),用来更新已有的数据。

-- delete(Uri, String, String[]),用来删除数据。

-- getType(Uri),用来返回数据的MIME类型。

这些函数的实现都比较简单,这里我们就不详细介绍了,主要解释五个要点。

第一点是我们在ArticlesProvider类的内部中定义了一个DBHelper类,它继承于SQLiteOpenHelper类,它用是用辅助我们操作数据库的。使用这个DBHelper类来辅助操作数据库的好处是只有当我们第一次对数据库时行操作时,系统才会执行打开数据库文件的操作。拿我们这个例子来说,只有第三方应用程序第一次调用query、insert、update或者delete函数来操作数据库时,我们才会真正去打开相应的数据库文件。这样在onCreate函数里,就不用执行打开数据库的操作,因为这是一个耗时的操作,而在onCreate函数中,要避免执行这些耗时的操作。

第二点是设置URI匹配规则。因为我们是根据URI来操作数据库的,不同的URI对应不同的操作,所以我们首先要定义好URI匹配规则,这样,当我们获得一个URI时,就能快速地判断出要如何去操作数据库。设置URI匹配规则的代码如下所示:

view plain
  1. privatestaticfinalUriMatcheruriMatcher;
  2. static{
  3. uriMatcher=newUriMatcher(UriMatcher.NO_MATCH);
  4. uriMatcher.addURI(Articles.AUTHORITY,"item",Articles.ITEM);
  5. uriMatcher.addURI(Articles.AUTHORITY,"item/#",Articles.ITEM_ID);
  6. uriMatcher.addURI(Articles.AUTHORITY,"pos/#",Articles.ITEM_POS);
  7. }

在创建UriMatcher对象uriMatcher时,我们传给构造函数的参数为UriMatcher.NO_MATCH,它表示当uriMatcher不能匹配指定的URI时,就返回代码UriMatcher.NO_MATCH。接下来增加了三个匹配规则,分别是content://shy.luo.providers.articles/item、content://shy.luo.providers.articles/item/#和content://shy.luo.providers.articles/pos/#,它们的匹配码分别是Articles.ITEM、Articles.ITEM_ID和Articles.ITEM_POS,其中,符号#表示匹配任何数字。

第三点是SQLiteQueryBuilder的使用。在query函数中,我们使用SQLiteQueryBuilder来辅助数据库查询操作,使用这个类的好处是我们可以不把数据库表的字段暴露出来,而是提供别名给第三方应用程序使用,这样就可以把数据库表内部设计隐藏起来,方便后续扩展和维护。列别名到真实列名的映射是由下面这个HashMap成员变量来实现的:

view plain
  1. privatestaticfinalHashMap<String,String>articleProjectionMap;
  2. static{
  3. articleProjectionMap=newHashMap<String,String>();
  4. articleProjectionMap.put(Articles.ID,Articles.ID);
  5. articleProjectionMap.put(Articles.TITLE,Articles.TITLE);
  6. articleProjectionMap.put(Articles.ABSTRACT,Articles.ABSTRACT);
  7. articleProjectionMap.put(Articles.URL,Articles.URL);
  8. }
在上面的put函数中,第一个参数表示列的别名,第二个参数表示列的真实名称。在这个例子中,我们把列的别名和和真实名称都设置成一样的。

第四点是数据更新机制的使用。执行insert、update和delete三个函数时,都会导致数据库中的数据发生变化,所以这时候要通过调用ContentResolver接口的notifyChange函数来通知那些注册了监控特定URI的ContentObserver对象,使得它们可以相应地执行一些处理,例如更新数据在界面上的显示。在query函数中,最终返回给调用者的是一个Cursor,调用者获得这个Cursor以后,可以通过它的deleteRow或者commitUpdates来执行一些更新数据库的操作,这时候也要通知那些注册了相应的URI的ContentObserver来作相应的处理,因此,这里在返回Cursor之前,要通过Cursor类的setNotificationUri函数来把当前上下文的ContentResolver对象保存到Curosr里面去,以便当通过这个Cursor来改变数据库中的数据时,可以通知相应的ContentObserver来处理。不过这种用法已经过时了,即不建议通过这个Cursor来改变数据库的数据,要把Cursor中的数据看作是只读数据。这里调用Cursor类的setNotificationUri函数还有另外一个作用,我们注意到它的第二个参数uri,对应的是Cursor中的内容,当把这个uri传给Cursor时,Cursor就会注册自己的ContentObserver来监控这个uri对应的数据的变化。一旦这个uri对应的数据发生变化,这个Cursor对应的数据就不是再新的了,这时候就需要采取一些操作来更新内容了。

第五点我们实现了ContentProvider的call函数。这个函数是一个未公开的函数,第三方应用程序只有Android源代码环境下开发,才能使用这个函数。设计这个函数的目的是什么呢?我们知道,当我们需要从Content Provider中获得数据时,一般都是要通过调用它的query函数来获得的,而这个函数将数据放在Cursor中来返回给调用者。以前面一篇文章Android应用程序组件Content Provider简要介绍和学习计划中,我们提到,Content Provider传给第三方应用程序的数据,是通过匿名共享内存来传输的。当要传输的数据量大的时候,使用匿名共享内存来传输数据是有好处的,它可以减入数据的拷贝,提高传输效率。但是,当要传输的数据量小时,使用匿名共享内存来作为媒介就有点用牛刀来杀鸡的味道了,因为匿名共享内存并不是免费的午餐,系统创建和匿名共享内存也是有开销的。因此,Content Provider提供了call函数来让第三方应用程序来获取一些自定义数据,这些数据一般都比较小,例如,只是传输一个整数,这样就可以用较小的代价来达到相同的数据传输的目的。

至此,ArticlesProvider的源代码就分析完了,下面我们还要在AndroidManifest.xml文件中配置这个ArticlesProvider类才能正常使用。AndroidManifest.xml文件的内容如下所示:

view plain
  1. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  2. package="shy.luo.providers.articles">
  3. <applicationandroid:process="shy.luo.process.article"
  4. android:label="@string/app_label"
  5. android:icon="@drawable/app_icon">
  6. <providerandroid:name="ArticlesProvider"
  7. android:authorities="shy.luo.providers.articles"
  8. android:label="@string/provider_label"
  9. android:multiprocess="false">
  10. </provider>
  11. </application>
  12. </manifest>
在配置Content Provider的时候,最重要的就是要指定它的authorities属性了,只有配置了这个属性,第三方应用程序才能通过它来找到这个Content Provider。这要需要注意的,这里配置的authorities属性的值是和我们前面在Articles.java文件中定义的AUTHORITY常量的值是一致的。另外一个属性multiprocess是一个布尔值,它表示这个Content Provider是否可以在每个客户进程中创建一个实例,这样做的目的是为了减少进程间通信的开销。这里我们为了减少不必要的内存开销,把属性multiprocess的值设置为false,使得系统只能有一个Content Provider实例存在,它运行在自己的进程中。在这个配置文件里面,我们还可以设置这个Content Provider的访问权限,这里我们为了简单起见,就不设置权限了。有关Content Provider的访问权限的设置,可以参考官方文档http://developer.android.com/guide/topics/manifest/provider-element.html。

这个应用程序使用到的字符串资源定义在res/values/strings.xml文件中,它的内容如下所示:

view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <resources>
  3. <stringname="app_label">ArticlesStorage</string>
  4. <stringname="provider_label">Articles</string>
  5. </resources>
由于Content Provider类型的应用程序是没有用户界面的,因此,我们不需要在res/layout目录下为程序准备界面配置文件。

程序的编译脚本Android.mk的内容如下所示:

view plain
  1. LOCAL_PATH:=$(callmy-dir)
  2. include$(CLEAR_VARS)
  3. LOCAL_MODULE_TAGS:=optional
  4. LOCAL_SRC_FILES:=$(callall-subdir-java-files)
  5. LOCAL_PACKAGE_NAME:=ArticlesProvider
  6. include$(BUILD_PACKAGE)
下面我们就可以参照如何单独编译Android源代码中的模块一文来编译和打包这个应用程序了:
view plain
  1. USER-NAME@MACHINE-NAME:~/Android$mmmpackages/experimental/ArticlesProvider
  2. USER-NAME@MACHINE-NAME:~/Android$makesnod

这样,打包好的Android系统镜像文件system.img就包含我们这里所创建的ArticlesProvider应用程序了。

前面说过,在Articles.java文件中定义的常量是要给第三方应用程序使用的,那么我们是不是直接把这个源文件交给第三方呢?这样就显得太不专业了,第三方拿到这个文件后,还必须要放在shy/luo/providers/articles目录下或者要把这个Articles类所在的package改掉才能正常使用。正确的做法是把编译好的Articles.java文件打包成一个jar文件交给第三方使用。编译ArticlesProvider这个应用程序成功后,生成的中间文件放在out/target/common/obj/APPS/ArticlesProvider_intermediates目录下,我们进入到这个目录中,然后执后下面的命令把Articles.class文件提取出来:

view plain
  1. USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$jar-xvfclasses.jarshy/luo/providers/articles/Articles.class

然后再单独打包这个Articles.class文件:

view plain
  1. USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$jar-cvfArticlesProvider.jar./shy
这样,我们得到的ArticlesProvider.jar文件就包含了Articles.java这个文件中定义的常量了,第三方拿到这个文件后,就可以开发自己的应用程序来访问我们在ArticlesProvider这个Content Provider中保存的数据了。接下来我们就介绍调用这个ArticlesProvider来获取数据的第三方应用程序Article。

2. Article应用程序的实现

首先是参照前面的ArticlesProvider工程,在packages/experimental目录下建立工程文件目录Article。这个应用程序的作用是用来管理ArticlesProvider应用程序中保存的文章信息的,因此,它需要获得相应的Content Provider接口来访问ArticlesProvider中的数据。我们首先在工程目录Article下面创建一个libs目录,把上面得到的ArticlesProvider.jar放在libs目录下,后面我们在编译脚本的时候,再把它引用到工程上来。下面我们就开始分析这个应用程序的实现。

这个应用程序的主界面MainActivity包含了一个ListView控件,用来显示从ArticlesProvider中得到的文章信息条目,在这个主界面上,可以浏览、增加、删除和更新文章信息。当需要增加、删除或者更新文章信息时,就会跳到另外一个界面ArticleActivity中去执行具体的操作。为了方便开发,我们把每一个文章信息条目封装成了一个Article类,并且把与ArticlesProvider进交互的操作都通过ArticlesAdapter类来实现。下面介绍每一个类的具本实现。

下面是Article类的实现,它实现在src/shy/luo/Article.java文件中:

view plain
  1. packageshy.luo.article;
  2. publicclassArticle{
  3. privateintid;
  4. privateStringtitle;
  5. privateStringabs;
  6. privateStringurl;
  7. publicArticle(intid,Stringtitle,Stringabs,Stringurl){
  8. this.id=id;
  9. this.title=title;
  10. this.abs=abs;
  11. this.url=url;
  12. }
  13. publicvoidsetId(intid){
  14. this.id=id;
  15. }
  16. publicintgetId(){
  17. returnthis.id;
  18. }
  19. publicvoidsetTitle(Stringtitle){
  20. this.title=title;
  21. }
  22. publicStringgetTitle(){
  23. returnthis.title;
  24. }
  25. publicvoidsetAbstract(Stringabs){
  26. this.abs=abs;
  27. }
  28. publicStringgetAbstract(){
  29. returnthis.abs;
  30. }
  31. publicvoidsetUrl(Stringurl){
  32. this.url=url;
  33. }
  34. publicStringgetUrl(){
  35. returnthis.url;
  36. }
  37. }
下面是ArticlesAdapter类的实现,它实现在src/shy/luo/ArticlesAdapter.java文件中:
view plain
  1. packageshy.luo.article;
  2. importjava.util.LinkedList;
  3. importshy.luo.providers.articles.Articles;
  4. importandroid.content.ContentResolver;
  5. importandroid.content.ContentUris;
  6. importandroid.content.ContentValues;
  7. importandroid.content.Context;
  8. importandroid.content.IContentProvider;
  9. importandroid.database.Cursor;
  10. importandroid.net.Uri;
  11. importandroid.os.Bundle;
  12. importandroid.os.RemoteException;
  13. importandroid.util.Log;
  14. publicclassArticlesAdapter{
  15. privatestaticfinalStringLOG_TAG="shy.luo.article.ArticlesAdapter";
  16. privateContentResolverresolver=null;
  17. publicArticlesAdapter(Contextcontext){
  18. resolver=context.getContentResolver();
  19. }
  20. publiclonginsertArticle(Articlearticle){
  21. ContentValuesvalues=newContentValues();
  22. values.put(Articles.TITLE,article.getTitle());
  23. values.put(Articles.ABSTRACT,article.getAbstract());
  24. values.put(Articles.URL,article.getUrl());
  25. Uriuri=resolver.insert(Articles.CONTENT_URI,values);
  26. StringitemId=uri.getPathSegments().get(1);
  27. returnInteger.valueOf(itemId).longValue();
  28. }
  29. publicbooleanupdateArticle(Articlearticle){
  30. Uriuri=ContentUris.withAppendedId(Articles.CONTENT_URI,article.getId());
  31. ContentValuesvalues=newContentValues();
  32. values.put(Articles.TITLE,article.getTitle());
  33. values.put(Articles.ABSTRACT,article.getAbstract());
  34. values.put(Articles.URL,article.getUrl());
  35. intcount=resolver.update(uri,values,null,null);
  36. returncount>0;
  37. }
  38. publicbooleanremoveArticle(intid){
  39. Uriuri=ContentUris.withAppendedId(Articles.CONTENT_URI,id);
  40. intcount=resolver.delete(uri,null,null);
  41. returncount>0;
  42. }
  43. publicLinkedList<Article>getAllArticles(){
  44. LinkedList<Article>articles=newLinkedList<Article>();
  45. String[]projection=newString[]{
  46. Articles.ID,
  47. Articles.TITLE,
  48. Articles.ABSTRACT,
  49. Articles.URL
  50. };
  51. Cursorcursor=resolver.query(Articles.CONTENT_URI,projection,null,null,Articles.DEFAULT_SORT_ORDER);
  52. if(cursor.moveToFirst()){
  53. do{
  54. intid=cursor.getInt(0);
  55. Stringtitle=cursor.getString(1);
  56. Stringabs=cursor.getString(2);
  57. Stringurl=cursor.getString(3);
  58. Articlearticle=newArticle(id,title,abs,url);
  59. articles.add(article);
  60. }while(cursor.moveToNext());
  61. }
  62. returnarticles;
  63. }
  64. publicintgetArticleCount(){
  65. intcount=0;
  66. try{
  67. IContentProviderprovider=resolver.acquireProvider(Articles.CONTENT_URI);
  68. Bundlebundle=provider.call(Articles.METHOD_GET_ITEM_COUNT,null,null);
  69. count=bundle.getInt(Articles.KEY_ITEM_COUNT,0);
  70. }catch(RemoteExceptione){
  71. e.printStackTrace();
  72. }
  73. returncount;
  74. }
  75. publicArticlegetArticleById(intid){
  76. Uriuri=ContentUris.withAppendedId(Articles.CONTENT_URI,id);
  77. String[]projection=newString[]{
  78. Articles.ID,
  79. Articles.TITLE,
  80. Articles.ABSTRACT,
  81. Articles.URL
  82. };
  83. Cursorcursor=resolver.query(uri,projection,null,null,Articles.DEFAULT_SORT_ORDER);
  84. Log.i(LOG_TAG,"cursor.moveToFirst");
  85. if(!cursor.moveToFirst()){
  86. returnnull;
  87. }
  88. Stringtitle=cursor.getString(1);
  89. Stringabs=cursor.getString(2);
  90. Stringurl=cursor.getString(3);
  91. returnnewArticle(id,title,abs,url);
  92. }
  93. publicArticlegetArticleByPos(intpos){
  94. Uriuri=ContentUris.withAppendedId(Articles.CONTENT_POS_URI,pos);
  95. String[]projection=newString[]{
  96. Articles.ID,
  97. Articles.TITLE,
  98. Articles.ABSTRACT,
  99. Articles.URL
  100. };
  101. Cursorcursor=resolver.query(uri,projection,null,null,Articles.DEFAULT_SORT_ORDER);
  102. if(!cursor.moveToFirst()){
  103. returnnull;
  104. }
  105. intid=cursor.getInt(0);
  106. Stringtitle=cursor.getString(1);
  107. Stringabs=cursor.getString(2);
  108. Stringurl=cursor.getString(3);
  109. returnnewArticle(id,title,abs,url);
  110. }
  111. }
这个类首先在构造函数里面获得应用程序上下文的ContentResolver接口,然后通过就可以通过这个接口来访问ArticlesProvider中的文章信息了。成员函数insertArticle、updateArticle和removeArticle分别用来新增、更新和删除一个文章信息条目;成员函数getAllArticlese用来获取所有的文章信息;成员函数getArticleById和getArticleByPos分别根据文章的ID和位置来获得具体文章信息条目;成员函数getArticleCount直接使用ContentProvider的未公开接口call来获得文章信息条目的数量,注意,这个函数要源代码环境下编译才能通过。

下面是程序主界面MainActivity类的实现,它实现在src/shy/luo/article/MainActivity.java文件中:

view plain
  1. packageshy.luo.article;
  2. importshy.luo.providers.articles.Articles;
  3. importandroid.app.Activity;
  4. importandroid.content.Context;
  5. importandroid.content.Intent;
  6. importandroid.database.ContentObserver;
  7. importandroid.os.Bundle;
  8. importandroid.os.Handler;
  9. importandroid.util.Log;
  10. importandroid.view.LayoutInflater;
  11. importandroid.view.View;
  12. importandroid.view.ViewGroup;
  13. importandroid.widget.AdapterView;
  14. importandroid.widget.BaseAdapter;
  15. importandroid.widget.Button;
  16. importandroid.widget.ListView;
  17. importandroid.widget.TextView;
  18. publicclassMainActivityextendsActivityimplementsView.OnClickListener,AdapterView.OnItemClickListener{
  19. privatefinalstaticStringLOG_TAG="shy.luo.article.MainActivity";
  20. privatefinalstaticintADD_ARTICAL_ACTIVITY=1;
  21. privatefinalstaticintEDIT_ARTICAL_ACTIVITY=2;
  22. privateArticlesAdapteraa=null;
  23. privateArticleAdapteradapter=null;
  24. privateArticleObserverobserver=null;
  25. privateListViewarticleList=null;
  26. privateButtonaddButton=null;
  27. @Override
  28. publicvoidonCreate(BundlesavedInstanceState){
  29. super.onCreate(savedInstanceState);
  30. setContentView(R.layout.main);
  31. aa=newArticlesAdapter(this);
  32. articleList=(ListView)findViewById(R.id.listview_article);
  33. adapter=newArticleAdapter(this);
  34. articleList.setAdapter(adapter);
  35. articleList.setOnItemClickListener(this);
  36. observer=newArticleObserver(newHandler());
  37. getContentResolver().registerContentObserver(Articles.CONTENT_URI,true,observer);
  38. addButton=(Button)findViewById(R.id.button_add);
  39. addButton.setOnClickListener(this);
  40. Log.i(LOG_TAG,"MainActivityCreated");
  41. }
  42. @Override
  43. publicvoidonDestroy(){
  44. super.onDestroy();
  45. getContentResolver().unregisterContentObserver(observer);
  46. }
  47. @Override
  48. publicvoidonClick(Viewv){
  49. if(v.equals(addButton)){
  50. Intentintent=newIntent(this,ArticleActivity.class);
  51. startActivityForResult(intent,ADD_ARTICAL_ACTIVITY);
  52. }
  53. }
  54. @Override
  55. publicvoidonItemClick(AdapterView<?>parent,Viewview,intpos,longid){
  56. Intentintent=newIntent(this,ArticleActivity.class);
  57. Articlearticle=aa.getArticleByPos(pos);
  58. intent.putExtra(Articles.ID,article.getId());
  59. intent.putExtra(Articles.TITLE,article.getTitle());
  60. intent.putExtra(Articles.ABSTRACT,article.getAbstract());
  61. intent.putExtra(Articles.URL,article.getUrl());
  62. startActivityForResult(intent,EDIT_ARTICAL_ACTIVITY);
  63. }
  64. @Override
  65. publicvoidonActivityResult(intrequestCode,intresultCode,Intentdata){
  66. super.onActivityResult(requestCode,resultCode,data);
  67. switch(requestCode){
  68. caseADD_ARTICAL_ACTIVITY:{
  69. if(resultCode==Activity.RESULT_OK){
  70. Stringtitle=data.getStringExtra(Articles.TITLE);
  71. Stringabs=data.getStringExtra(Articles.ABSTRACT);
  72. Stringurl=data.getStringExtra(Articles.URL);
  73. Articlearticle=newArticle(-1,title,abs,url);
  74. aa.insertArticle(article);
  75. }
  76. break;
  77. }
  78. caseEDIT_ARTICAL_ACTIVITY:{
  79. if(resultCode==Activity.RESULT_OK){
  80. intaction=data.getIntExtra(ArticleActivity.EDIT_ARTICLE_ACTION,-1);
  81. if(action==ArticleActivity.MODIFY_ARTICLE){
  82. intid=data.getIntExtra(Articles.ID,-1);
  83. Stringtitle=data.getStringExtra(Articles.TITLE);
  84. Stringabs=data.getStringExtra(Articles.ABSTRACT);
  85. Stringurl=data.getStringExtra(Articles.URL);
  86. Articlearticle=newArticle(id,title,abs,url);
  87. aa.updateArticle(article);
  88. }elseif(action==ArticleActivity.DELETE_ARTICLE){
  89. intid=data.getIntExtra(Articles.ID,-1);
  90. aa.removeArticle(id);
  91. }
  92. }
  93. break;
  94. }
  95. }
  96. }
  97. privateclassArticleObserverextendsContentObserver{
  98. publicArticleObserver(Handlerhandler){
  99. super(handler);
  100. }
  101. @Override
  102. publicvoidonChange(booleanselfChange){
  103. adapter.notifyDataSetChanged();
  104. }
  105. }
  106. privateclassArticleAdapterextendsBaseAdapter{
  107. privateLayoutInflaterinflater;
  108. publicArticleAdapter(Contextcontext){
  109. inflater=LayoutInflater.from(context);
  110. }
  111. @Override
  112. publicintgetCount(){
  113. returnaa.getArticleCount();
  114. }
  115. @Override
  116. publicObjectgetItem(intpos){
  117. returnaa.getArticleByPos(pos);
  118. }
  119. @Override
  120. publiclonggetItemId(intpos){
  121. returnaa.getArticleByPos(pos).getId();
  122. }
  123. @Override
  124. publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
  125. Articlearticle=(Article)getItem(position);
  126. if(convertView==null){
  127. convertView=inflater.inflate(R.layout.item,null);
  128. }
  129. TextViewtitleView=(TextView)convertView.findViewById(R.id.textview_article_title);
  130. titleView.setText("Title:"+article.getTitle());
  131. TextViewabstractView=(TextView)convertView.findViewById(R.id.textview_article_abstract);
  132. abstractView.setText("Abstract:"+article.getAbstract());
  133. TextViewurlView=(TextView)convertView.findViewById(R.id.textview_article_url);
  134. urlView.setText("URL:"+article.getUrl());
  135. returnconvertView;
  136. }
  137. }
  138. }
在应用程序的主界面中,我们使用一个ListView来显示文章信息条目,这个ListView的数据源由ArticleAdapter类来提供,而ArticleAdapter类又是通过ArticlesAdapter类来获得ArticlesProvider中的文章信息的。在MainActivity的onCreate函数,我们还通过应用程序上下文的ContentResolver接口来注册了一个ArticleObserver对象来监控ArticlesProvider中的文章信息。一旦ArticlesProvider中的文章信息发生变化,就会通过ArticleAdapter类来实时更新ListView中的文章信息。

下面是ArticleActivity类的实现,它实现在src/shy/luo/article/ArticleActivity.java文件中:

view plain
  1. packageshy.luo.article;
  2. importshy.luo.providers.articles.Articles;
  3. importandroid.app.Activity;
  4. importandroid.content.Intent;
  5. importandroid.os.Bundle;
  6. importandroid.util.Log;
  7. importandroid.view.View;
  8. importandroid.widget.Button;
  9. importandroid.widget.EditText;
  10. publicclassArticleActivityextendsActivityimplementsView.OnClickListener{
  11. privatefinalstaticStringLOG_TAG="shy.luo.article.ArticleActivity";
  12. publicfinalstaticStringEDIT_ARTICLE_ACTION="EDIT_ARTICLE_ACTION";
  13. publicfinalstaticintMODIFY_ARTICLE=1;
  14. publicfinalstaticintDELETE_ARTICLE=2;
  15. privateintarticleId=-1;
  16. privateEditTexttitleEdit=null;
  17. privateEditTextabstractEdit=null;
  18. privateEditTexturlEdit=null;
  19. privateButtonaddButton=null;
  20. privateButtonmodifyButton=null;
  21. privateButtondeleteButton=null;
  22. privateButtoncancelButton=null;
  23. @Override
  24. publicvoidonCreate(BundlesavedInstanceState){
  25. super.onCreate(savedInstanceState);
  26. setContentView(R.layout.article);
  27. titleEdit=(EditText)findViewById(R.id.edit_article_title);
  28. abstractEdit=(EditText)findViewById(R.id.edit_article_abstract);
  29. urlEdit=(EditText)findViewById(R.id.edit_article_url);
  30. addButton=(Button)findViewById(R.id.button_add_article);
  31. addButton.setOnClickListener(this);
  32. modifyButton=(Button)findViewById(R.id.button_modify);
  33. modifyButton.setOnClickListener(this);
  34. deleteButton=(Button)findViewById(R.id.button_delete);
  35. deleteButton.setOnClickListener(this);
  36. cancelButton=(Button)findViewById(R.id.button_cancel);
  37. cancelButton.setOnClickListener(this);
  38. Intentintent=getIntent();
  39. articleId=intent.getIntExtra(Articles.ID,-1);
  40. if(articleId!=-1){
  41. Stringtitle=intent.getStringExtra(Articles.TITLE);
  42. titleEdit.setText(title);
  43. Stringabs=intent.getStringExtra(Articles.ABSTRACT);
  44. abstractEdit.setText(abs);
  45. Stringurl=intent.getStringExtra(Articles.URL);
  46. urlEdit.setText(url);
  47. addButton.setVisibility(View.GONE);
  48. }else{
  49. modifyButton.setVisibility(View.GONE);
  50. deleteButton.setVisibility(View.GONE);
  51. }
  52. Log.i(LOG_TAG,"ArticleActivityCreated");
  53. }
  54. @Override
  55. publicvoidonClick(Viewv){
  56. if(v.equals(addButton)){
  57. Stringtitle=titleEdit.getText().toString();
  58. Stringabs=abstractEdit.getText().toString();
  59. Stringurl=urlEdit.getText().toString();
  60. Intentresult=newIntent();
  61. result.putExtra(Articles.TITLE,title);
  62. result.putExtra(Articles.ABSTRACT,abs);
  63. result.putExtra(Articles.URL,url);
  64. setResult(Activity.RESULT_OK,result);
  65. finish();
  66. }elseif(v.equals(modifyButton)){
  67. Stringtitle=titleEdit.getText().toString();
  68. Stringabs=abstractEdit.getText().toString();
  69. Stringurl=urlEdit.getText().toString();
  70. Intentresult=newIntent();
  71. result.putExtra(Articles.ID,articleId);
  72. result.putExtra(Articles.TITLE,title);
  73. result.putExtra(Articles.ABSTRACT,abs);
  74. result.putExtra(Articles.URL,url);
  75. result.putExtra(EDIT_ARTICLE_ACTION,MODIFY_ARTICLE);
  76. setResult(Activity.RESULT_OK,result);
  77. finish();
  78. }elseif(v.equals(deleteButton)){
  79. Intentresult=newIntent();
  80. result.putExtra(Articles.ID,articleId);
  81. result.putExtra(EDIT_ARTICLE_ACTION,DELETE_ARTICLE);
  82. setResult(Activity.RESULT_OK,result);
  83. finish();
  84. }elseif(v.equals(cancelButton)){
  85. setResult(Activity.RESULT_CANCELED,null);
  86. finish();
  87. }
  88. }
  89. }
在ArticleActivity窗口中,我们可以执行新增、更新和删除文章信息的操作。如果启动ArticleActivity时,没有把文章ID传进来,就说明要执行操作是新增文章信息;如果启动ArticleActivity时,把文章ID和其它信自都传进来了,就说明要执行的操作是更新或者删除文章,根据用户在界面点击的是更新按钮还是删除按钮来确定。

程序使用到的界面文件定义在res/layout目录下,其中,main.xml文件定义MainActivity的界面,它的内容如下所示:

view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="fill_parent"
  5. android:layout_height="fill_parent"
  6. android:gravity="bottom">
  7. <ListView
  8. android:id="@+id/listview_article"
  9. android:layout_width="fill_parent"
  10. android:layout_height="wrap_content"
  11. android:layout_weight="1"
  12. android:background="@drawable/border"
  13. android:choiceMode="singleChoice">
  14. </ListView>
  15. <LinearLayout
  16. android:orientation="horizontal"
  17. android:layout_height="wrap_content"
  18. android:layout_width="match_parent"
  19. android:gravity="center"
  20. android:layout_marginTop="10dp">
  21. <Button
  22. android:id="@+id/button_add"
  23. android:layout_width="wrap_content"
  24. android:layout_height="wrap_content"
  25. android:paddingLeft="15dp"
  26. android:paddingRight="15dp"
  27. android:text="@string/add">
  28. </Button>
  29. </LinearLayout>
  30. </LinearLayout>
item.xml文件定义了ListView中每一个文章信息条目的显示界面,它的内容如下所示:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:orientation="vertical"
  5. android:layout_width="fill_parent"
  6. android:layout_height="wrap_content">
  7. <TextView
  8. android:id="@+id/textview_article_title"
  9. android:layout_width="fill_parent"
  10. android:layout_height="wrap_content">
  11. </TextView>
  12. <TextView
  13. android:id="@+id/textview_article_abstract"
  14. android:layout_width="fill_parent"
  15. android:layout_height="wrap_content">
  16. </TextView>
  17. <TextView
  18. android:id="@+id/textview_article_url"
  19. android:layout_width="fill_parent"
  20. android:layout_height="wrap_content"
  21. android:layout_marginBottom="10dp">
  22. </TextView>
  23. </LinearLayout>
article.xml文件定义了ArticleActivity的界面,它的内容如下所示:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="fill_parent"
  5. android:layout_height="fill_parent"
  6. android:gravity="center">
  7. <LinearLayout
  8. android:orientation="horizontal"
  9. android:layout_height="wrap_content"
  10. android:layout_width="fill_parent">
  11. <TextView
  12. android:layout_width="wrap_content"
  13. android:layout_height="wrap_content"
  14. android:layout_marginRight="24dp"
  15. android:text="@string/title">
  16. </TextView>
  17. <EditText
  18. android:id="@+id/edit_article_title"
  19. android:layout_width="fill_parent"
  20. android:layout_height="wrap_content">
  21. </EditText>
  22. </LinearLayout>
  23. <LinearLayout
  24. android:orientation="horizontal"
  25. android:layout_height="wrap_content"
  26. android:layout_width="fill_parent">
  27. <TextView
  28. android:layout_width="wrap_content"
  29. android:layout_height="wrap_content"
  30. android:text="@string/abs">
  31. </TextView>
  32. <EditText
  33. android:id="@+id/edit_article_abstract"
  34. android:layout_width="fill_parent"
  35. android:layout_height="wrap_content">
  36. </EditText>
  37. </LinearLayout>
  38. <LinearLayout
  39. android:orientation="horizontal"
  40. android:layout_height="wrap_content"
  41. android:layout_width="fill_parent">
  42. <TextView
  43. android:layout_width="wrap_content"
  44. android:layout_height="wrap_content"
  45. android:layout_marginRight="27dp"
  46. android:text="@string/url">
  47. </TextView>
  48. <EditText
  49. android:id="@+id/edit_article_url"
  50. android:layout_width="fill_parent"
  51. android:layout_height="wrap_content">
  52. </EditText>
  53. </LinearLayout>
  54. <LinearLayout
  55. android:orientation="horizontal"
  56. android:layout_height="wrap_content"
  57. android:layout_width="match_parent"
  58. android:gravity="center"
  59. android:layout_marginTop="10dp">
  60. <Button
  61. android:id="@+id/button_modify"
  62. android:layout_width="wrap_content"
  63. android:layout_height="wrap_content"
  64. android:text="@string/modify">
  65. </Button>
  66. <Button
  67. android:id="@+id/button_delete"
  68. android:layout_width="wrap_content"
  69. android:layout_height="wrap_content"
  70. android:text="@string/delete">
  71. </Button>
  72. <Button
  73. android:id="@+id/button_add_article"
  74. android:layout_width="wrap_content"
  75. android:layout_height="wrap_content"
  76. android:paddingLeft="16dp"
  77. android:paddingRight="16dp"
  78. android:text="@string/add">
  79. </Button>
  80. <Button
  81. android:id="@+id/button_cancel"
  82. android:layout_width="wrap_content"
  83. android:layout_height="wrap_content"
  84. android:text="@string/cancel">
  85. </Button>
  86. </LinearLayout>
  87. </LinearLayout>
在res/drawable目录下,有一个border.xml文件定义了MainActivity界面上的ListView的背景,它的内容如下所示:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <shapexmlns:android="http://schemas.android.com/apk/res/android"
  3. android:shape="rectangle">
  4. <solidandroid:color="#ff0000ff"/>
  5. <strokeandroid:width="1dp"
  6. android:color="#000000">
  7. </stroke>
  8. <paddingandroid:left="7dp"
  9. android:top="7dp"
  10. android:right="7dp"
  11. android:bottom="7dp">
  12. </padding>
  13. <cornersandroid:radius="10dp"/>
  14. </shape>
程序使用到的字符串资源文件定义在res/values/strings.xml文件中,它的内容如下所示:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <resources>
  3. <stringname="app_name">Article</string>
  4. <stringname="article">Article</string>
  5. <stringname="add">Add</string>
  6. <stringname="modify">Modify</string>
  7. <stringname="delete">Delete</string>
  8. <stringname="title">Title:</string>
  9. <stringname="abs">Abstract:</string>
  10. <stringname="url">URL:</string>
  11. <stringname="ok">OK</string>
  12. <stringname="cancel">Cancel</string>
  13. </resources>
接下来再来看程序的配置文件AndroidManifest.xml:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  3. package="shy.luo.article"
  4. android:versionCode="1"
  5. android:versionName="1.0">
  6. <applicationandroid:icon="@drawable/icon"android:label="@string/app_name">
  7. <activityandroid:name=".MainActivity"
  8. android:label="@string/app_name">
  9. <intent-filter>
  10. <actionandroid:name="android.intent.action.MAIN"/>
  11. <categoryandroid:name="android.intent.category.LAUNCHER"/>
  12. </intent-filter>
  13. </activity>
  14. <activity
  15. android:name=".ArticleActivity"
  16. android:label="@string/article">
  17. </activity>
  18. </application>
  19. </manifest>
编译脚本Android.mk的内容如下所示:
view plain
  1. LOCAL_PATH:=$(callmy-dir)
  2. include$(CLEAR_VARS)
  3. LOCAL_MODULE_TAGS:=optional
  4. LOCAL_STATIC_JAVA_LIBRARIES:=libArticlesProvider
  5. LOCAL_SRC_FILES:=$(callall-subdir-java-files)
  6. LOCAL_PACKAGE_NAME:=Article
  7. include$(BUILD_PACKAGE)
  8. ###################################################
  9. include$(CLEAR_VARS)
  10. LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES:=libArticlesProvider:./libs/ArticlesProvider.jar
  11. include$(BUILD_MULTI_PREBUILT)
这个编译脚本包含了两个部分的指令,一个是把libs目录下的预编译静态库ArticlesProvider.jar编译成一本地静态库libArticlesProvider,它的相关库文件保存在out/target/common/obj/JAVA_LIBRARIES/libArticlesProvider_intermediates目录下;另一个就是编译我们的程序Article了,它通过LOCAL_STATIC_JAVA_LIBRARIES变量来引用前面的libArticlesProvider库,这个库包含了所有我们用来访问ArticlesProvider这个Content Provider中的数据的常量。

下面我们就可以编译和打包这个应用程序了:

view plain
  1. USER-NAME@MACHINE-NAME:~/Android$mmmpackages/experimental/Article
  2. USER-NAME@MACHINE-NAME:~/Android$makesnod

这样,打包好的Android系统镜像文件system.img就包含我们这里所创建的Article应用程序了。

最后,就是运行模拟器来运行我们的例子了。关于如何在Android源代码工程中运行模拟器,请参考在Ubuntu上下载、编译和安装Android最新源代码一文。
执行以下命令启动模拟器:
view plain
  1. USER-NAME@MACHINE-NAME:~/Android$emulator
这个应用程序的主界面如下图所示:

点击下面的Add按钮,可以添加新的文章信息条目:


在前一个界面的文件列表中,点击某一个文章条目,便可以更新或者删除文章信息条目:



这样,Content Provider的使用实例就介绍完了。这篇文章的目的是使读者对Content Provider有一个大概的了解和感性的认识,在下一篇文章中,我们将详细介绍Article应用程序是如何获得ArticlesProvider这个ContentProvider接口的,只有获得了这个接口之后,Article应用程序才能访问ArticlesProvider的数据,敬请关注。


上文简要介绍了Android应用程序组件Content Provider在应用程序间共享数据的原理,但是没有进一步研究它的实现。本文将实现两个应用程序,其中一个以Content Provider的形式来提供数据访问入口,另一个通过这个Content Provider来访问这些数据。本文的例子不仅可以为下文分析Content Provider的实现原理准备好使用情景,还可以学习到它的一个未公开接口。

本文中的应用程序是按照上一篇文章Android应用程序组件Content Provider简要介绍和学习计划中提到的一般应用程序架构方法来设计的。本文包含两个应用程序,其中,第一个应用程序命名为ArticlesProvider,它使用了SQLite数据库来维护一个文章信息列表,同时,它定义了访问这个文章信息列表的URI,这样,我们就可以通过一个Content Provider组件来向第三方应用程序提供访问这个文章信息列表的接口;第二个应用程序命名为Article,它提供了管理保存在ArticlesProvider应用程序中的文章信息的界面入口,在这个应用程序中,用户可以添加、删除和修改这些文章信息。接下来我们就分别介绍这两个应用程序的实现。

1. ArticlesProvider应用程序的实现

首先是参照在Ubuntu上为Android系统内置Java应用程序测试Application Frameworks层的硬件服务一文,在packages/experimental目录下建立工程文件目录ArticlesProvider。在继续介绍这个应用程序的实现之前,我们先介绍一下这个应用程序用来保存文章信息的数据库的设计。

我们知道,在Android系统中,内置了一款轻型的数据库SQLite。SQLite是专门为嵌入式产品而设计的,它具有占用资源低的特点,而且是开源的,非常适合在Android平台中使用,关于SQLite的更多信息可以访问官方网站http://www.sqlite.org。

ArticlesProvider应用程序就是使用SQLite来作为数据库保存文章信息的,数据库文件命名为Articles.db,它里面只有一张表ArticlesTable,表的结构如下所示:

-------------------------------------------------------------

| --_id --| -- _title -- | --_abstrat --| -- _url --|

-------------------------------------------------------------

| | | | |

它由四个字段表示,第一个字段_id表示文章的ID,类型为自动递增的integer,它作为表的key值;第二个字段_title表示文章的题目,类型为text;第三个字段_abstract表示文章的摘要,类型为text;第四个字段_url表示文章的URL,类型为text。注意,当我们打算将数据库表的某一列的数据作为一个数据行的ID时,就约定它的列名为_id。这是因为我们经常需要从数据库中获取一批数据,这些数据以Cursor的形式返回,对这些返回来的数据我们一般用一个ListView来显示,而这个ListView需要一个数据适配器Adapter来作为数据源,这时候就我们就可以以这个Cursor来构造一个Adapter。有些Adapter,例如android.widget.CursorAdapter,它们在实现自己的getItemId成员函数来获取指定数据行的ID时,就必须要从这个Cursor中相应的行里面取出列名为_id的字段的内容出来作为这个数据行的ID返回给调用者。当然,我们不在数据库表中定义这个_id列名也是可以的,不过这样从数据库中查询数据后得到的Cursor适合性就变差了,因此,建议我们在设计数据库表时,尽量设置其中一个列名字_id,并且保证这一列的内容是在数据库表中是唯一的。

下面我们就开始介绍这个应用程序的实现了。这个应用程序只有两个源文件,分别是Articles.java和ArticlesProvider,都是放在shy.luo.providers.articles这个package下面。在Articles.java文件里面,主要是定义了一些常量,例如用来访问文章信息数据的URI、MIME(Multipurpose Internet Mail Extensions)类型以及格式等,这些常量是第三方应用程序访问这些文章信息数据时要使用到的,因此,我们把它定义在一个单独的文件中,稍后我们会介绍如果把这个Articles.java文件打包成一个jar文件,然后第三方应用程序就可以引用这个常量了,这样也避免了直接把这个源代码文件暴露给第三方应用程序。

源文件Articles.java位于src/shy/luo/providers/articles目录下,它的内容如下所示:

view plain
  1. packageshy.luo.providers.articles;
  2. importandroid.net.Uri;
  3. publicclassArticles{
  4. /*DataField*/
  5. publicstaticfinalStringID="_id";
  6. publicstaticfinalStringTITLE="_title";
  7. publicstaticfinalStringABSTRACT="_abstract";
  8. publicstaticfinalStringURL="_url";
  9. /*Defaultsortorder*/
  10. publicstaticfinalStringDEFAULT_SORT_ORDER="_idasc";
  11. /*CallMethod*/
  12. publicstaticfinalStringMETHOD_GET_ITEM_COUNT="METHOD_GET_ITEM_COUNT";
  13. publicstaticfinalStringKEY_ITEM_COUNT="KEY_ITEM_COUNT";
  14. /*Authority*/
  15. publicstaticfinalStringAUTHORITY="shy.luo.providers.articles";
  16. /*MatchCode*/
  17. publicstaticfinalintITEM=1;
  18. publicstaticfinalintITEM_ID=2;
  19. publicstaticfinalintITEM_POS=3;
  20. /*MIME*/
  21. publicstaticfinalStringCONTENT_TYPE="vnd.android.cursor.dir/vnd.shy.luo.article";
  22. publicstaticfinalStringCONTENT_ITEM_TYPE="vnd.android.cursor.item/vnd.shy.luo.article";
  23. /*ContentURI*/
  24. publicstaticfinalUriCONTENT_URI=Uri.parse("content://"+AUTHORITY+"/item");
  25. publicstaticfinalUriCONTENT_POS_URI=Uri.parse("content://"+AUTHORITY+"/pos");
  26. }
ID、TITLE、ABSTRACT和URL四个常量前面已经解释过了,它是我们用来保存文章信息的数据表的四个列名;DEFAULT_SORT_ORDER常量是调用ContentProvider接口的query函数来查询数据时用的,它表示对查询结果按照_id列的值从小到大排列;METHOD_GET_ITEM_COUNT和KEY_ITEM_COUNT两个常量是调用ContentProvider接口的一个未公开函数call来查询数据时用的,它类似于微软COM中的IDispatch接口的Invoke函数,使用这个call函数时,传入参数METHOD_GET_ITEM_COUNT表示我们要调用我们自定义的ContentProvider子类中的getItemCount函数来获取数据库中的文章信息条目的数量,结果放在一个Bundle中以KEY_ITEM_COUNT为关键字的域中。

剩下的常量都是跟数据URI相关的,这个需要详细解释一下。URI的全称是Universal Resource Identifier,即通用资源标志符,通过它用来唯一标志某个资源在网络中的位置,它的结构和我们常见的HTTP形式URL是一样的,其实我们可以把常见的HTTP形式的URL看成是URI结构的一个实例,URI是在更高一个层次上的抽象。在Android系统中,它也定义了自己的用来定痊某个特定的Content Provider的URI结构,它通常由四个组件来组成,如下所示:

[content://][shy.luo.providers.articles][/item][/123]

|------A------|-----------------B-------------------|---C---|---D--|

A组件称为Scheme,它固定为content://,表示它后面的路径所表示的资源是由Content Provider来提供的。

B组件称为Authority,它唯一地标识了一个特定的Content Provider,因此,这部分内容一般使用Content Provider所在的package来命名,使得它是唯一的。

C组件称为资源路径,它表示所请求的资源的类型,这部分内容是可选的。如果我们自己所实现的Content Provider只提供一种类型的资源访问,那么这部分内部就可以忽略;如果我们自己实现的Content Provider同时提供了多种类型的资源访问,那么这部分内容就不可以忽略了。例如,我们有两种电脑资源可以提供给用户访问,一种是笔记本电脑,一种是平板电脑,我们就把分别它们定义为notebook和pad;如果我们想进一步按照系统类型来进一步细分这两种电脑资源,对笔记本电脑来说,一种是安装了windows系统的,一种是安装了linux系统的,我们就分别把它们定义为notebook/windows和notebook/linux;对平板电脑来说,一种是安装了ios系统的,一种是安装了android系统的,我们就分别把它们定义为pad/ios和pad/android。

D组件称为资源ID,它表示所请求的是一个特定的资源,它通常是一个数字,对应前面我们所介绍的数据库表中的_id字段的内容,它唯一地标志了某一种资源下的一个特定的实例。继续以前面的电脑资源为例,如果我们请求的是编号为123的装了android系统的平板电脑,我们就把它定义为pad/android/123。当忽略这部分内容时,它有可能是表示请求某一种资源下的所有实例,取决于我们的URI匹配规则,后面我们将会进一步解释如何设置URI匹配规则。

回到上面的Articles.java源文件中,我们定义了两个URI,分别用COTENT_URI和CONTENT_POS_URI两个常量来表示,它们的Authority组件均指定为shy.luo.providers.articles。其中,COTENT_URI常量表示的URI表示是通过ID来访问文章信息的,而CONTENT_POS_URI常量表示的URI表示是通过位置来访问文章信息的。例如,content://shy.luo.providers.articles/item表示访问所有的文章信息条目;content://shy.luo.providers.articles/item/123表示只访问ID值为123的文章信息条目;content://shy.luo.providers.articles/pos/1表示访问数据库表中的第1条文章信息条目,这条文章信息条目的ID值不一定为1。通过常量CONTENT_POS_URI来访问文章信息条目时,必须要指定位置,这也是我们设置的URI匹配规则来指定的,后面我们将会看到。

此外,我们还需要定义与URI对应的资源的MIME类型。每个MIME类型由两部分组成,前面是数据的大类别,后面定义具体的种类。在Content Provider中,URI所对应的资源的MIME类型的大类别根据同时访问的资源的数量分为两种,对于访问单个资源的URI,它的大类别就为vnd.android.cursor.item,而对于同时访问多个资源的URI,它的大类别就为vnd.android.cursor.dir。Content Provider的URI所对应的资源的MIME类型的具体类别就需要由Content Provider的提供者来设置了,它的格式一般为vnd.[company name].[resource type]的形式。例如,在我们的例子中,CONTENT_TYPE和COTENT_ITEM_TYPE两个常量分别定义了两种MIME类型,它们的大类别分别为vnd.android.cursor.dir和vnd.android.cursor.item,而具体类别均为vdn.shy.luo.article,其中shy.luo就是表示公司名了,而article表示资源的类型为文章。这两个MIME类型常量主要是在实现ContentProvider的getType函数时用到的,后面我们将会看到。

最后,ITEM、ITEM_ID和POS_ID三个常量分别定了三个URI匹配规则的匹配码。如果URI的形式为content://shy.luo.providers.articles/item,则匹配规则返回的匹配码为ITEM;如果URI的形式为content://shy.luo.providers.articles/item/#,其中#表示任意一个数字,则匹配规则返回的匹配码为ITEM_ID;如果URI的形式为#也是表示任意一个数字,则匹配规则返回的匹配码为ITEM_POS。这三个常量的用法我们在后面也将会看到。

这样,Articles.java文件的内容就介绍完了。下面我们再接着介绍位于src/shy/luo/providers/articles目录下的ArticlesProvider.java文件,它的内容如下所示:

view plain
  1. importjava.util.HashMap;
  2. importandroid.content.ContentValues;
  3. importandroid.content.Context;
  4. importandroid.content.UriMatcher;
  5. importandroid.content.ContentProvider;
  6. importandroid.content.ContentUris;
  7. importandroid.content.ContentResolver;
  8. importandroid.database.Cursor;
  9. importandroid.database.sqlite.SQLiteDatabase;
  10. importandroid.database.sqlite.SQLiteDatabase.CursorFactory;
  11. importandroid.database.sqlite.SQLiteException;
  12. importandroid.database.sqlite.SQLiteOpenHelper;
  13. importandroid.database.sqlite.SQLiteQueryBuilder;
  14. importandroid.net.Uri;
  15. importandroid.os.Bundle;
  16. importandroid.text.TextUtils;
  17. importandroid.util.Log;
  18. publicclassArticlesProviderextendsContentProvider{
  19. privatestaticfinalStringLOG_TAG="shy.luo.providers.articles.ArticlesProvider";
  20. privatestaticfinalStringDB_NAME="Articles.db";
  21. privatestaticfinalStringDB_TABLE="ArticlesTable";
  22. privatestaticfinalintDB_VERSION=1;
  23. privatestaticfinalStringDB_CREATE="createtable"+DB_TABLE+
  24. "("+Articles.ID+"integerprimarykeyautoincrement,"+
  25. Articles.TITLE+"textnotnull,"+
  26. Articles.ABSTRACT+"textnotnull,"+
  27. Articles.URL+"textnotnull);";
  28. privatestaticfinalUriMatcheruriMatcher;
  29. static{
  30. uriMatcher=newUriMatcher(UriMatcher.NO_MATCH);
  31. uriMatcher.addURI(Articles.AUTHORITY,"item",Articles.ITEM);
  32. uriMatcher.addURI(Articles.AUTHORITY,"item/#",Articles.ITEM_ID);
  33. uriMatcher.addURI(Articles.AUTHORITY,"pos/#",Articles.ITEM_POS);
  34. }
  35. privatestaticfinalHashMap<String,String>articleProjectionMap;
  36. static{
  37. articleProjectionMap=newHashMap<String,String>();
  38. articleProjectionMap.put(Articles.ID,Articles.ID);
  39. articleProjectionMap.put(Articles.TITLE,Articles.TITLE);
  40. articleProjectionMap.put(Articles.ABSTRACT,Articles.ABSTRACT);
  41. articleProjectionMap.put(Articles.URL,Articles.URL);
  42. }
  43. privateDBHelperdbHelper=null;
  44. privateContentResolverresolver=null;
  45. @Override
  46. publicbooleanonCreate(){
  47. Contextcontext=getContext();
  48. resolver=context.getContentResolver();
  49. dbHelper=newDBHelper(context,DB_NAME,null,DB_VERSION);
  50. Log.i(LOG_TAG,"ArticlesProviderCreate");
  51. returntrue;
  52. }
  53. @Override
  54. publicStringgetType(Uriuri){
  55. switch(uriMatcher.match(uri)){
  56. caseArticles.ITEM:
  57. returnArticles.CONTENT_TYPE;
  58. caseArticles.ITEM_ID:
  59. caseArticles.ITEM_POS:
  60. returnArticles.CONTENT_ITEM_TYPE;
  61. default:
  62. thrownewIllegalArgumentException("ErrorUri:"+uri);
  63. }
  64. }
  65. @Override
  66. publicUriinsert(Uriuri,ContentValuesvalues){
  67. if(uriMatcher.match(uri)!=Articles.ITEM){
  68. thrownewIllegalArgumentException("ErrorUri:"+uri);
  69. }
  70. SQLiteDatabasedb=dbHelper.getWritableDatabase();
  71. longid=db.insert(DB_TABLE,Articles.ID,values);
  72. if(id<0){
  73. thrownewSQLiteException("Unabletoinsert"+values+"for"+uri);
  74. }
  75. UrinewUri=ContentUris.withAppendedId(uri,id);
  76. resolver.notifyChange(newUri,null);
  77. returnnewUri;
  78. }
  79. @Override
  80. publicintupdate(Uriuri,ContentValuesvalues,Stringselection,String[]selectionArgs){
  81. SQLiteDatabasedb=dbHelper.getWritableDatabase();
  82. intcount=0;
  83. switch(uriMatcher.match(uri)){
  84. caseArticles.ITEM:{
  85. count=db.update(DB_TABLE,values,selection,selectionArgs);
  86. break;
  87. }
  88. caseArticles.ITEM_ID:{
  89. Stringid=uri.getPathSegments().get(1);
  90. count=db.update(DB_TABLE,values,Articles.ID+"="+id
  91. +(!TextUtils.isEmpty(selection)?"and("+selection+')':""),selectionArgs);
  92. break;
  93. }
  94. default:
  95. thrownewIllegalArgumentException("ErrorUri:"+uri);
  96. }
  97. resolver.notifyChange(uri,null);
  98. returncount;
  99. }
  100. @Override
  101. publicintdelete(Uriuri,Stringselection,String[]selectionArgs){
  102. SQLiteDatabasedb=dbHelper.getWritableDatabase();
  103. intcount=0;
  104. switch(uriMatcher.match(uri)){
  105. caseArticles.ITEM:{
  106. count=db.delete(DB_TABLE,selection,selectionArgs);
  107. break;
  108. }
  109. caseArticles.ITEM_ID:{
  110. Stringid=uri.getPathSegments().get(1);
  111. count=db.delete(DB_TABLE,Articles.ID+"="+id
  112. +(!TextUtils.isEmpty(selection)?"and("+selection+')':""),selectionArgs);
  113. break;
  114. }
  115. default:
  116. thrownewIllegalArgumentException("ErrorUri:"+uri);
  117. }
  118. resolver.notifyChange(uri,null);
  119. returncount;
  120. }
  121. @Override
  122. publicCursorquery(Uriuri,String[]projection,Stringselection,String[]selectionArgs,StringsortOrder){
  123. Log.i(LOG_TAG,"ArticlesProvider.query:"+uri);
  124. SQLiteDatabasedb=dbHelper.getReadableDatabase();
  125. SQLiteQueryBuildersqlBuilder=newSQLiteQueryBuilder();
  126. Stringlimit=null;
  127. switch(uriMatcher.match(uri)){
  128. caseArticles.ITEM:{
  129. sqlBuilder.setTables(DB_TABLE);
  130. sqlBuilder.setProjectionMap(articleProjectionMap);
  131. break;
  132. }
  133. caseArticles.ITEM_ID:{
  134. Stringid=uri.getPathSegments().get(1);
  135. sqlBuilder.setTables(DB_TABLE);
  136. sqlBuilder.setProjectionMap(articleProjectionMap);
  137. sqlBuilder.appendWhere(Articles.ID+"="+id);
  138. break;
  139. }
  140. caseArticles.ITEM_POS:{
  141. Stringpos=uri.getPathSegments().get(1);
  142. sqlBuilder.setTables(DB_TABLE);
  143. sqlBuilder.setProjectionMap(articleProjectionMap);
  144. limit=pos+",1";
  145. break;
  146. }
  147. default:
  148. thrownewIllegalArgumentException("ErrorUri:"+uri);
  149. }
  150. Cursorcursor=sqlBuilder.query(db,projection,selection,selectionArgs,null,null,TextUtils.isEmpty(sortOrder)?Articles.DEFAULT_SORT_ORDER:sortOrder,limit);
  151. cursor.setNotificationUri(resolver,uri);
  152. returncursor;
  153. }
  154. @Override
  155. publicBundlecall(Stringmethod,Stringrequest,Bundleargs){
  156. Log.i(LOG_TAG,"ArticlesProvider.call:"+method);
  157. if(method.equals(Articles.METHOD_GET_ITEM_COUNT)){
  158. returngetItemCount();
  159. }
  160. thrownewIllegalArgumentException("Errormethodcall:"+method);
  161. }
  162. privateBundlegetItemCount(){
  163. Log.i(LOG_TAG,"ArticlesProvider.getItemCount");
  164. SQLiteDatabasedb=dbHelper.getReadableDatabase();
  165. Cursorcursor=db.rawQuery("selectcount(*)from"+DB_TABLE,null);
  166. intcount=0;
  167. if(cursor.moveToFirst()){
  168. count=cursor.getInt(0);
  169. }
  170. Bundlebundle=newBundle();
  171. bundle.putInt(Articles.KEY_ITEM_COUNT,count);
  172. cursor.close();
  173. db.close();
  174. returnbundle;
  175. }
  176. privatestaticclassDBHelperextendsSQLiteOpenHelper{
  177. publicDBHelper(Contextcontext,Stringname,CursorFactoryfactory,intversion){
  178. super(context,name,factory,version);
  179. }
  180. @Override
  181. publicvoidonCreate(SQLiteDatabasedb){
  182. db.execSQL(DB_CREATE);
  183. }
  184. @Override
  185. publicvoidonUpgrade(SQLiteDatabasedb,intoldVersion,intnewVersion){
  186. db.execSQL("DROPTABLEIFEXISTS"+DB_TABLE);
  187. onCreate(db);
  188. }
  189. }
  190. }

我们在实现自己的Content Provider时,必须继承于ContentProvider类,并且实现以下六个函数:

-- onCreate(),用来执行一些初始化的工作。

-- query(Uri, String[], String, String[], String),用来返回数据给调用者。

-- insert(Uri, ContentValues),用来插入新的数据。

-- update(Uri, ContentValues, String, String[]),用来更新已有的数据。

-- delete(Uri, String, String[]),用来删除数据。

-- getType(Uri),用来返回数据的MIME类型。

这些函数的实现都比较简单,这里我们就不详细介绍了,主要解释五个要点。

第一点是我们在ArticlesProvider类的内部中定义了一个DBHelper类,它继承于SQLiteOpenHelper类,它用是用辅助我们操作数据库的。使用这个DBHelper类来辅助操作数据库的好处是只有当我们第一次对数据库时行操作时,系统才会执行打开数据库文件的操作。拿我们这个例子来说,只有第三方应用程序第一次调用query、insert、update或者delete函数来操作数据库时,我们才会真正去打开相应的数据库文件。这样在onCreate函数里,就不用执行打开数据库的操作,因为这是一个耗时的操作,而在onCreate函数中,要避免执行这些耗时的操作。

第二点是设置URI匹配规则。因为我们是根据URI来操作数据库的,不同的URI对应不同的操作,所以我们首先要定义好URI匹配规则,这样,当我们获得一个URI时,就能快速地判断出要如何去操作数据库。设置URI匹配规则的代码如下所示:

view plain
  1. privatestaticfinalUriMatcheruriMatcher;
  2. static{
  3. uriMatcher=newUriMatcher(UriMatcher.NO_MATCH);
  4. uriMatcher.addURI(Articles.AUTHORITY,"item",Articles.ITEM);
  5. uriMatcher.addURI(Articles.AUTHORITY,"item/#",Articles.ITEM_ID);
  6. uriMatcher.addURI(Articles.AUTHORITY,"pos/#",Articles.ITEM_POS);
  7. }

在创建UriMatcher对象uriMatcher时,我们传给构造函数的参数为UriMatcher.NO_MATCH,它表示当uriMatcher不能匹配指定的URI时,就返回代码UriMatcher.NO_MATCH。接下来增加了三个匹配规则,分别是content://shy.luo.providers.articles/item、content://shy.luo.providers.articles/item/#和content://shy.luo.providers.articles/pos/#,它们的匹配码分别是Articles.ITEM、Articles.ITEM_ID和Articles.ITEM_POS,其中,符号#表示匹配任何数字。

第三点是SQLiteQueryBuilder的使用。在query函数中,我们使用SQLiteQueryBuilder来辅助数据库查询操作,使用这个类的好处是我们可以不把数据库表的字段暴露出来,而是提供别名给第三方应用程序使用,这样就可以把数据库表内部设计隐藏起来,方便后续扩展和维护。列别名到真实列名的映射是由下面这个HashMap成员变量来实现的:

view plain
  1. privatestaticfinalHashMap<String,String>articleProjectionMap;
  2. static{
  3. articleProjectionMap=newHashMap<String,String>();
  4. articleProjectionMap.put(Articles.ID,Articles.ID);
  5. articleProjectionMap.put(Articles.TITLE,Articles.TITLE);
  6. articleProjectionMap.put(Articles.ABSTRACT,Articles.ABSTRACT);
  7. articleProjectionMap.put(Articles.URL,Articles.URL);
  8. }
在上面的put函数中,第一个参数表示列的别名,第二个参数表示列的真实名称。在这个例子中,我们把列的别名和和真实名称都设置成一样的。

第四点是数据更新机制的使用。执行insert、update和delete三个函数时,都会导致数据库中的数据发生变化,所以这时候要通过调用ContentResolver接口的notifyChange函数来通知那些注册了监控特定URI的ContentObserver对象,使得它们可以相应地执行一些处理,例如更新数据在界面上的显示。在query函数中,最终返回给调用者的是一个Cursor,调用者获得这个Cursor以后,可以通过它的deleteRow或者commitUpdates来执行一些更新数据库的操作,这时候也要通知那些注册了相应的URI的ContentObserver来作相应的处理,因此,这里在返回Cursor之前,要通过Cursor类的setNotificationUri函数来把当前上下文的ContentResolver对象保存到Curosr里面去,以便当通过这个Cursor来改变数据库中的数据时,可以通知相应的ContentObserver来处理。不过这种用法已经过时了,即不建议通过这个Cursor来改变数据库的数据,要把Cursor中的数据看作是只读数据。这里调用Cursor类的setNotificationUri函数还有另外一个作用,我们注意到它的第二个参数uri,对应的是Cursor中的内容,当把这个uri传给Cursor时,Cursor就会注册自己的ContentObserver来监控这个uri对应的数据的变化。一旦这个uri对应的数据发生变化,这个Cursor对应的数据就不是再新的了,这时候就需要采取一些操作来更新内容了。

第五点我们实现了ContentProvider的call函数。这个函数是一个未公开的函数,第三方应用程序只有Android源代码环境下开发,才能使用这个函数。设计这个函数的目的是什么呢?我们知道,当我们需要从Content Provider中获得数据时,一般都是要通过调用它的query函数来获得的,而这个函数将数据放在Cursor中来返回给调用者。以前面一篇文章Android应用程序组件Content Provider简要介绍和学习计划中,我们提到,Content Provider传给第三方应用程序的数据,是通过匿名共享内存来传输的。当要传输的数据量大的时候,使用匿名共享内存来传输数据是有好处的,它可以减入数据的拷贝,提高传输效率。但是,当要传输的数据量小时,使用匿名共享内存来作为媒介就有点用牛刀来杀鸡的味道了,因为匿名共享内存并不是免费的午餐,系统创建和匿名共享内存也是有开销的。因此,Content Provider提供了call函数来让第三方应用程序来获取一些自定义数据,这些数据一般都比较小,例如,只是传输一个整数,这样就可以用较小的代价来达到相同的数据传输的目的。

至此,ArticlesProvider的源代码就分析完了,下面我们还要在AndroidManifest.xml文件中配置这个ArticlesProvider类才能正常使用。AndroidManifest.xml文件的内容如下所示:

view plain
  1. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  2. package="shy.luo.providers.articles">
  3. <applicationandroid:process="shy.luo.process.article"
  4. android:label="@string/app_label"
  5. android:icon="@drawable/app_icon">
  6. <providerandroid:name="ArticlesProvider"
  7. android:authorities="shy.luo.providers.articles"
  8. android:label="@string/provider_label"
  9. android:multiprocess="false">
  10. </provider>
  11. </application>
  12. </manifest>
在配置Content Provider的时候,最重要的就是要指定它的authorities属性了,只有配置了这个属性,第三方应用程序才能通过它来找到这个Content Provider。这要需要注意的,这里配置的authorities属性的值是和我们前面在Articles.java文件中定义的AUTHORITY常量的值是一致的。另外一个属性multiprocess是一个布尔值,它表示这个Content Provider是否可以在每个客户进程中创建一个实例,这样做的目的是为了减少进程间通信的开销。这里我们为了减少不必要的内存开销,把属性multiprocess的值设置为false,使得系统只能有一个Content Provider实例存在,它运行在自己的进程中。在这个配置文件里面,我们还可以设置这个Content Provider的访问权限,这里我们为了简单起见,就不设置权限了。有关Content Provider的访问权限的设置,可以参考官方文档http://developer.android.com/guide/topics/manifest/provider-element.html。

这个应用程序使用到的字符串资源定义在res/values/strings.xml文件中,它的内容如下所示:

view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <resources>
  3. <stringname="app_label">ArticlesStorage</string>
  4. <stringname="provider_label">Articles</string>
  5. </resources>
由于Content Provider类型的应用程序是没有用户界面的,因此,我们不需要在res/layout目录下为程序准备界面配置文件。

程序的编译脚本Android.mk的内容如下所示:

view plain
  1. LOCAL_PATH:=$(callmy-dir)
  2. include$(CLEAR_VARS)
  3. LOCAL_MODULE_TAGS:=optional
  4. LOCAL_SRC_FILES:=$(callall-subdir-java-files)
  5. LOCAL_PACKAGE_NAME:=ArticlesProvider
  6. include$(BUILD_PACKAGE)
下面我们就可以参照如何单独编译Android源代码中的模块一文来编译和打包这个应用程序了:
view plain
  1. USER-NAME@MACHINE-NAME:~/Android$mmmpackages/experimental/ArticlesProvider
  2. USER-NAME@MACHINE-NAME:~/Android$makesnod

这样,打包好的Android系统镜像文件system.img就包含我们这里所创建的ArticlesProvider应用程序了。

前面说过,在Articles.java文件中定义的常量是要给第三方应用程序使用的,那么我们是不是直接把这个源文件交给第三方呢?这样就显得太不专业了,第三方拿到这个文件后,还必须要放在shy/luo/providers/articles目录下或者要把这个Articles类所在的package改掉才能正常使用。正确的做法是把编译好的Articles.java文件打包成一个jar文件交给第三方使用。编译ArticlesProvider这个应用程序成功后,生成的中间文件放在out/target/common/obj/APPS/ArticlesProvider_intermediates目录下,我们进入到这个目录中,然后执后下面的命令把Articles.class文件提取出来:

view plain
  1. USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$jar-xvfclasses.jarshy/luo/providers/articles/Articles.class

然后再单独打包这个Articles.class文件:

view plain
  1. USER-NAME@MACHINE-NAME:~/Android/out/target/common/obj/APPS/ArticlesProvider_intermediates$jar-cvfArticlesProvider.jar./shy
这样,我们得到的ArticlesProvider.jar文件就包含了Articles.java这个文件中定义的常量了,第三方拿到这个文件后,就可以开发自己的应用程序来访问我们在ArticlesProvider这个Content Provider中保存的数据了。接下来我们就介绍调用这个ArticlesProvider来获取数据的第三方应用程序Article。

2. Article应用程序的实现

首先是参照前面的ArticlesProvider工程,在packages/experimental目录下建立工程文件目录Article。这个应用程序的作用是用来管理ArticlesProvider应用程序中保存的文章信息的,因此,它需要获得相应的Content Provider接口来访问ArticlesProvider中的数据。我们首先在工程目录Article下面创建一个libs目录,把上面得到的ArticlesProvider.jar放在libs目录下,后面我们在编译脚本的时候,再把它引用到工程上来。下面我们就开始分析这个应用程序的实现。

这个应用程序的主界面MainActivity包含了一个ListView控件,用来显示从ArticlesProvider中得到的文章信息条目,在这个主界面上,可以浏览、增加、删除和更新文章信息。当需要增加、删除或者更新文章信息时,就会跳到另外一个界面ArticleActivity中去执行具体的操作。为了方便开发,我们把每一个文章信息条目封装成了一个Article类,并且把与ArticlesProvider进交互的操作都通过ArticlesAdapter类来实现。下面介绍每一个类的具本实现。

下面是Article类的实现,它实现在src/shy/luo/Article.java文件中:

view plain
  1. packageshy.luo.article;
  2. publicclassArticle{
  3. privateintid;
  4. privateStringtitle;
  5. privateStringabs;
  6. privateStringurl;
  7. publicArticle(intid,Stringtitle,Stringabs,Stringurl){
  8. this.id=id;
  9. this.title=title;
  10. this.abs=abs;
  11. this.url=url;
  12. }
  13. publicvoidsetId(intid){
  14. this.id=id;
  15. }
  16. publicintgetId(){
  17. returnthis.id;
  18. }
  19. publicvoidsetTitle(Stringtitle){
  20. this.title=title;
  21. }
  22. publicStringgetTitle(){
  23. returnthis.title;
  24. }
  25. publicvoidsetAbstract(Stringabs){
  26. this.abs=abs;
  27. }
  28. publicStringgetAbstract(){
  29. returnthis.abs;
  30. }
  31. publicvoidsetUrl(Stringurl){
  32. this.url=url;
  33. }
  34. publicStringgetUrl(){
  35. returnthis.url;
  36. }
  37. }
下面是ArticlesAdapter类的实现,它实现在src/shy/luo/ArticlesAdapter.java文件中:
view plain
  1. packageshy.luo.article;
  2. importjava.util.LinkedList;
  3. importshy.luo.providers.articles.Articles;
  4. importandroid.content.ContentResolver;
  5. importandroid.content.ContentUris;
  6. importandroid.content.ContentValues;
  7. importandroid.content.Context;
  8. importandroid.content.IContentProvider;
  9. importandroid.database.Cursor;
  10. importandroid.net.Uri;
  11. importandroid.os.Bundle;
  12. importandroid.os.RemoteException;
  13. importandroid.util.Log;
  14. publicclassArticlesAdapter{
  15. privatestaticfinalStringLOG_TAG="shy.luo.article.ArticlesAdapter";
  16. privateContentResolverresolver=null;
  17. publicArticlesAdapter(Contextcontext){
  18. resolver=context.getContentResolver();
  19. }
  20. publiclonginsertArticle(Articlearticle){
  21. ContentValuesvalues=newContentValues();
  22. values.put(Articles.TITLE,article.getTitle());
  23. values.put(Articles.ABSTRACT,article.getAbstract());
  24. values.put(Articles.URL,article.getUrl());
  25. Uriuri=resolver.insert(Articles.CONTENT_URI,values);
  26. StringitemId=uri.getPathSegments().get(1);
  27. returnInteger.valueOf(itemId).longValue();
  28. }
  29. publicbooleanupdateArticle(Articlearticle){
  30. Uriuri=ContentUris.withAppendedId(Articles.CONTENT_URI,article.getId());
  31. ContentValuesvalues=newContentValues();
  32. values.put(Articles.TITLE,article.getTitle());
  33. values.put(Articles.ABSTRACT,article.getAbstract());
  34. values.put(Articles.URL,article.getUrl());
  35. intcount=resolver.update(uri,values,null,null);
  36. returncount>0;
  37. }
  38. publicbooleanremoveArticle(intid){
  39. Uriuri=ContentUris.withAppendedId(Articles.CONTENT_URI,id);
  40. intcount=resolver.delete(uri,null,null);
  41. returncount>0;
  42. }
  43. publicLinkedList<Article>getAllArticles(){
  44. LinkedList<Article>articles=newLinkedList<Article>();
  45. String[]projection=newString[]{
  46. Articles.ID,
  47. Articles.TITLE,
  48. Articles.ABSTRACT,
  49. Articles.URL
  50. };
  51. Cursorcursor=resolver.query(Articles.CONTENT_URI,projection,null,null,Articles.DEFAULT_SORT_ORDER);
  52. if(cursor.moveToFirst()){
  53. do{
  54. intid=cursor.getInt(0);
  55. Stringtitle=cursor.getString(1);
  56. Stringabs=cursor.getString(2);
  57. Stringurl=cursor.getString(3);
  58. Articlearticle=newArticle(id,title,abs,url);
  59. articles.add(article);
  60. }while(cursor.moveToNext());
  61. }
  62. returnarticles;
  63. }
  64. publicintgetArticleCount(){
  65. intcount=0;
  66. try{
  67. IContentProviderprovider=resolver.acquireProvider(Articles.CONTENT_URI);
  68. Bundlebundle=provider.call(Articles.METHOD_GET_ITEM_COUNT,null,null);
  69. count=bundle.getInt(Articles.KEY_ITEM_COUNT,0);
  70. }catch(RemoteExceptione){
  71. e.printStackTrace();
  72. }
  73. returncount;
  74. }
  75. publicArticlegetArticleById(intid){
  76. Uriuri=ContentUris.withAppendedId(Articles.CONTENT_URI,id);
  77. String[]projection=newString[]{
  78. Articles.ID,
  79. Articles.TITLE,
  80. Articles.ABSTRACT,
  81. Articles.URL
  82. };
  83. Cursorcursor=resolver.query(uri,projection,null,null,Articles.DEFAULT_SORT_ORDER);
  84. Log.i(LOG_TAG,"cursor.moveToFirst");
  85. if(!cursor.moveToFirst()){
  86. returnnull;
  87. }
  88. Stringtitle=cursor.getString(1);
  89. Stringabs=cursor.getString(2);
  90. Stringurl=cursor.getString(3);
  91. returnnewArticle(id,title,abs,url);
  92. }
  93. publicArticlegetArticleByPos(intpos){
  94. Uriuri=ContentUris.withAppendedId(Articles.CONTENT_POS_URI,pos);
  95. String[]projection=newString[]{
  96. Articles.ID,
  97. Articles.TITLE,
  98. Articles.ABSTRACT,
  99. Articles.URL
  100. };
  101. Cursorcursor=resolver.query(uri,projection,null,null,Articles.DEFAULT_SORT_ORDER);
  102. if(!cursor.moveToFirst()){
  103. returnnull;
  104. }
  105. intid=cursor.getInt(0);
  106. Stringtitle=cursor.getString(1);
  107. Stringabs=cursor.getString(2);
  108. Stringurl=cursor.getString(3);
  109. returnnewArticle(id,title,abs,url);
  110. }
  111. }
这个类首先在构造函数里面获得应用程序上下文的ContentResolver接口,然后通过就可以通过这个接口来访问ArticlesProvider中的文章信息了。成员函数insertArticle、updateArticle和removeArticle分别用来新增、更新和删除一个文章信息条目;成员函数getAllArticlese用来获取所有的文章信息;成员函数getArticleById和getArticleByPos分别根据文章的ID和位置来获得具体文章信息条目;成员函数getArticleCount直接使用ContentProvider的未公开接口call来获得文章信息条目的数量,注意,这个函数要源代码环境下编译才能通过。

下面是程序主界面MainActivity类的实现,它实现在src/shy/luo/article/MainActivity.java文件中:

view plain
  1. packageshy.luo.article;
  2. importshy.luo.providers.articles.Articles;
  3. importandroid.app.Activity;
  4. importandroid.content.Context;
  5. importandroid.content.Intent;
  6. importandroid.database.ContentObserver;
  7. importandroid.os.Bundle;
  8. importandroid.os.Handler;
  9. importandroid.util.Log;
  10. importandroid.view.LayoutInflater;
  11. importandroid.view.View;
  12. importandroid.view.ViewGroup;
  13. importandroid.widget.AdapterView;
  14. importandroid.widget.BaseAdapter;
  15. importandroid.widget.Button;
  16. importandroid.widget.ListView;
  17. importandroid.widget.TextView;
  18. publicclassMainActivityextendsActivityimplementsView.OnClickListener,AdapterView.OnItemClickListener{
  19. privatefinalstaticStringLOG_TAG="shy.luo.article.MainActivity";
  20. privatefinalstaticintADD_ARTICAL_ACTIVITY=1;
  21. privatefinalstaticintEDIT_ARTICAL_ACTIVITY=2;
  22. privateArticlesAdapteraa=null;
  23. privateArticleAdapteradapter=null;
  24. privateArticleObserverobserver=null;
  25. privateListViewarticleList=null;
  26. privateButtonaddButton=null;
  27. @Override
  28. publicvoidonCreate(BundlesavedInstanceState){
  29. super.onCreate(savedInstanceState);
  30. setContentView(R.layout.main);
  31. aa=newArticlesAdapter(this);
  32. articleList=(ListView)findViewById(R.id.listview_article);
  33. adapter=newArticleAdapter(this);
  34. articleList.setAdapter(adapter);
  35. articleList.setOnItemClickListener(this);
  36. observer=newArticleObserver(newHandler());
  37. getContentResolver().registerContentObserver(Articles.CONTENT_URI,true,observer);
  38. addButton=(Button)findViewById(R.id.button_add);
  39. addButton.setOnClickListener(this);
  40. Log.i(LOG_TAG,"MainActivityCreated");
  41. }
  42. @Override
  43. publicvoidonDestroy(){
  44. super.onDestroy();
  45. getContentResolver().unregisterContentObserver(observer);
  46. }
  47. @Override
  48. publicvoidonClick(Viewv){
  49. if(v.equals(addButton)){
  50. Intentintent=newIntent(this,ArticleActivity.class);
  51. startActivityForResult(intent,ADD_ARTICAL_ACTIVITY);
  52. }
  53. }
  54. @Override
  55. publicvoidonItemClick(AdapterView<?>parent,Viewview,intpos,longid){
  56. Intentintent=newIntent(this,ArticleActivity.class);
  57. Articlearticle=aa.getArticleByPos(pos);
  58. intent.putExtra(Articles.ID,article.getId());
  59. intent.putExtra(Articles.TITLE,article.getTitle());
  60. intent.putExtra(Articles.ABSTRACT,article.getAbstract());
  61. intent.putExtra(Articles.URL,article.getUrl());
  62. startActivityForResult(intent,EDIT_ARTICAL_ACTIVITY);
  63. }
  64. @Override
  65. publicvoidonActivityResult(intrequestCode,intresultCode,Intentdata){
  66. super.onActivityResult(requestCode,resultCode,data);
  67. switch(requestCode){
  68. caseADD_ARTICAL_ACTIVITY:{
  69. if(resultCode==Activity.RESULT_OK){
  70. Stringtitle=data.getStringExtra(Articles.TITLE);
  71. Stringabs=data.getStringExtra(Articles.ABSTRACT);
  72. Stringurl=data.getStringExtra(Articles.URL);
  73. Articlearticle=newArticle(-1,title,abs,url);
  74. aa.insertArticle(article);
  75. }
  76. break;
  77. }
  78. caseEDIT_ARTICAL_ACTIVITY:{
  79. if(resultCode==Activity.RESULT_OK){
  80. intaction=data.getIntExtra(ArticleActivity.EDIT_ARTICLE_ACTION,-1);
  81. if(action==ArticleActivity.MODIFY_ARTICLE){
  82. intid=data.getIntExtra(Articles.ID,-1);
  83. Stringtitle=data.getStringExtra(Articles.TITLE);
  84. Stringabs=data.getStringExtra(Articles.ABSTRACT);
  85. Stringurl=data.getStringExtra(Articles.URL);
  86. Articlearticle=newArticle(id,title,abs,url);
  87. aa.updateArticle(article);
  88. }elseif(action==ArticleActivity.DELETE_ARTICLE){
  89. intid=data.getIntExtra(Articles.ID,-1);
  90. aa.removeArticle(id);
  91. }
  92. }
  93. break;
  94. }
  95. }
  96. }
  97. privateclassArticleObserverextendsContentObserver{
  98. publicArticleObserver(Handlerhandler){
  99. super(handler);
  100. }
  101. @Override
  102. publicvoidonChange(booleanselfChange){
  103. adapter.notifyDataSetChanged();
  104. }
  105. }
  106. privateclassArticleAdapterextendsBaseAdapter{
  107. privateLayoutInflaterinflater;
  108. publicArticleAdapter(Contextcontext){
  109. inflater=LayoutInflater.from(context);
  110. }
  111. @Override
  112. publicintgetCount(){
  113. returnaa.getArticleCount();
  114. }
  115. @Override
  116. publicObjectgetItem(intpos){
  117. returnaa.getArticleByPos(pos);
  118. }
  119. @Override
  120. publiclonggetItemId(intpos){
  121. returnaa.getArticleByPos(pos).getId();
  122. }
  123. @Override
  124. publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
  125. Articlearticle=(Article)getItem(position);
  126. if(convertView==null){
  127. convertView=inflater.inflate(R.layout.item,null);
  128. }
  129. TextViewtitleView=(TextView)convertView.findViewById(R.id.textview_article_title);
  130. titleView.setText("Title:"+article.getTitle());
  131. TextViewabstractView=(TextView)convertView.findViewById(R.id.textview_article_abstract);
  132. abstractView.setText("Abstract:"+article.getAbstract());
  133. TextViewurlView=(TextView)convertView.findViewById(R.id.textview_article_url);
  134. urlView.setText("URL:"+article.getUrl());
  135. returnconvertView;
  136. }
  137. }
  138. }
在应用程序的主界面中,我们使用一个ListView来显示文章信息条目,这个ListView的数据源由ArticleAdapter类来提供,而ArticleAdapter类又是通过ArticlesAdapter类来获得ArticlesProvider中的文章信息的。在MainActivity的onCreate函数,我们还通过应用程序上下文的ContentResolver接口来注册了一个ArticleObserver对象来监控ArticlesProvider中的文章信息。一旦ArticlesProvider中的文章信息发生变化,就会通过ArticleAdapter类来实时更新ListView中的文章信息。

下面是ArticleActivity类的实现,它实现在src/shy/luo/article/ArticleActivity.java文件中:

view plain
  1. packageshy.luo.article;
  2. importshy.luo.providers.articles.Articles;
  3. importandroid.app.Activity;
  4. importandroid.content.Intent;
  5. importandroid.os.Bundle;
  6. importandroid.util.Log;
  7. importandroid.view.View;
  8. importandroid.widget.Button;
  9. importandroid.widget.EditText;
  10. publicclassArticleActivityextendsActivityimplementsView.OnClickListener{
  11. privatefinalstaticStringLOG_TAG="shy.luo.article.ArticleActivity";
  12. publicfinalstaticStringEDIT_ARTICLE_ACTION="EDIT_ARTICLE_ACTION";
  13. publicfinalstaticintMODIFY_ARTICLE=1;
  14. publicfinalstaticintDELETE_ARTICLE=2;
  15. privateintarticleId=-1;
  16. privateEditTexttitleEdit=null;
  17. privateEditTextabstractEdit=null;
  18. privateEditTexturlEdit=null;
  19. privateButtonaddButton=null;
  20. privateButtonmodifyButton=null;
  21. privateButtondeleteButton=null;
  22. privateButtoncancelButton=null;
  23. @Override
  24. publicvoidonCreate(BundlesavedInstanceState){
  25. super.onCreate(savedInstanceState);
  26. setContentView(R.layout.article);
  27. titleEdit=(EditText)findViewById(R.id.edit_article_title);
  28. abstractEdit=(EditText)findViewById(R.id.edit_article_abstract);
  29. urlEdit=(EditText)findViewById(R.id.edit_article_url);
  30. addButton=(Button)findViewById(R.id.button_add_article);
  31. addButton.setOnClickListener(this);
  32. modifyButton=(Button)findViewById(R.id.button_modify);
  33. modifyButton.setOnClickListener(this);
  34. deleteButton=(Button)findViewById(R.id.button_delete);
  35. deleteButton.setOnClickListener(this);
  36. cancelButton=(Button)findViewById(R.id.button_cancel);
  37. cancelButton.setOnClickListener(this);
  38. Intentintent=getIntent();
  39. articleId=intent.getIntExtra(Articles.ID,-1);
  40. if(articleId!=-1){
  41. Stringtitle=intent.getStringExtra(Articles.TITLE);
  42. titleEdit.setText(title);
  43. Stringabs=intent.getStringExtra(Articles.ABSTRACT);
  44. abstractEdit.setText(abs);
  45. Stringurl=intent.getStringExtra(Articles.URL);
  46. urlEdit.setText(url);
  47. addButton.setVisibility(View.GONE);
  48. }else{
  49. modifyButton.setVisibility(View.GONE);
  50. deleteButton.setVisibility(View.GONE);
  51. }
  52. Log.i(LOG_TAG,"ArticleActivityCreated");
  53. }
  54. @Override
  55. publicvoidonClick(Viewv){
  56. if(v.equals(addButton)){
  57. Stringtitle=titleEdit.getText().toString();
  58. Stringabs=abstractEdit.getText().toString();
  59. Stringurl=urlEdit.getText().toString();
  60. Intentresult=newIntent();
  61. result.putExtra(Articles.TITLE,title);
  62. result.putExtra(Articles.ABSTRACT,abs);
  63. result.putExtra(Articles.URL,url);
  64. setResult(Activity.RESULT_OK,result);
  65. finish();
  66. }elseif(v.equals(modifyButton)){
  67. Stringtitle=titleEdit.getText().toString();
  68. Stringabs=abstractEdit.getText().toString();
  69. Stringurl=urlEdit.getText().toString();
  70. Intentresult=newIntent();
  71. result.putExtra(Articles.ID,articleId);
  72. result.putExtra(Articles.TITLE,title);
  73. result.putExtra(Articles.ABSTRACT,abs);
  74. result.putExtra(Articles.URL,url);
  75. result.putExtra(EDIT_ARTICLE_ACTION,MODIFY_ARTICLE);
  76. setResult(Activity.RESULT_OK,result);
  77. finish();
  78. }elseif(v.equals(deleteButton)){
  79. Intentresult=newIntent();
  80. result.putExtra(Articles.ID,articleId);
  81. result.putExtra(EDIT_ARTICLE_ACTION,DELETE_ARTICLE);
  82. setResult(Activity.RESULT_OK,result);
  83. finish();
  84. }elseif(v.equals(cancelButton)){
  85. setResult(Activity.RESULT_CANCELED,null);
  86. finish();
  87. }
  88. }
  89. }
在ArticleActivity窗口中,我们可以执行新增、更新和删除文章信息的操作。如果启动ArticleActivity时,没有把文章ID传进来,就说明要执行操作是新增文章信息;如果启动ArticleActivity时,把文章ID和其它信自都传进来了,就说明要执行的操作是更新或者删除文章,根据用户在界面点击的是更新按钮还是删除按钮来确定。

程序使用到的界面文件定义在res/layout目录下,其中,main.xml文件定义MainActivity的界面,它的内容如下所示:

view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="fill_parent"
  5. android:layout_height="fill_parent"
  6. android:gravity="bottom">
  7. <ListView
  8. android:id="@+id/listview_article"
  9. android:layout_width="fill_parent"
  10. android:layout_height="wrap_content"
  11. android:layout_weight="1"
  12. android:background="@drawable/border"
  13. android:choiceMode="singleChoice">
  14. </ListView>
  15. <LinearLayout
  16. android:orientation="horizontal"
  17. android:layout_height="wrap_content"
  18. android:layout_width="match_parent"
  19. android:gravity="center"
  20. android:layout_marginTop="10dp">
  21. <Button
  22. android:id="@+id/button_add"
  23. android:layout_width="wrap_content"
  24. android:layout_height="wrap_content"
  25. android:paddingLeft="15dp"
  26. android:paddingRight="15dp"
  27. android:text="@string/add">
  28. </Button>
  29. </LinearLayout>
  30. </LinearLayout>
item.xml文件定义了ListView中每一个文章信息条目的显示界面,它的内容如下所示:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:orientation="vertical"
  5. android:layout_width="fill_parent"
  6. android:layout_height="wrap_content">
  7. <TextView
  8. android:id="@+id/textview_article_title"
  9. android:layout_width="fill_parent"
  10. android:layout_height="wrap_content">
  11. </TextView>
  12. <TextView
  13. android:id="@+id/textview_article_abstract"
  14. android:layout_width="fill_parent"
  15. android:layout_height="wrap_content">
  16. </TextView>
  17. <TextView
  18. android:id="@+id/textview_article_url"
  19. android:layout_width="fill_parent"
  20. android:layout_height="wrap_content"
  21. android:layout_marginBottom="10dp">
  22. </TextView>
  23. </LinearLayout>
article.xml文件定义了ArticleActivity的界面,它的内容如下所示:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical"
  4. android:layout_width="fill_parent"
  5. android:layout_height="fill_parent"
  6. android:gravity="center">
  7. <LinearLayout
  8. android:orientation="horizontal"
  9. android:layout_height="wrap_content"
  10. android:layout_width="fill_parent">
  11. <TextView
  12. android:layout_width="wrap_content"
  13. android:layout_height="wrap_content"
  14. android:layout_marginRight="24dp"
  15. android:text="@string/title">
  16. </TextView>
  17. <EditText
  18. android:id="@+id/edit_article_title"
  19. android:layout_width="fill_parent"
  20. android:layout_height="wrap_content">
  21. </EditText>
  22. </LinearLayout>
  23. <LinearLayout
  24. android:orientation="horizontal"
  25. android:layout_height="wrap_content"
  26. android:layout_width="fill_parent">
  27. <TextView
  28. android:layout_width="wrap_content"
  29. android:layout_height="wrap_content"
  30. android:text="@string/abs">
  31. </TextView>
  32. <EditText
  33. android:id="@+id/edit_article_abstract"
  34. android:layout_width="fill_parent"
  35. android:layout_height="wrap_content">
  36. </EditText>
  37. </LinearLayout>
  38. <LinearLayout
  39. android:orientation="horizontal"
  40. android:layout_height="wrap_content"
  41. android:layout_width="fill_parent">
  42. <TextView
  43. android:layout_width="wrap_content"
  44. android:layout_height="wrap_content"
  45. android:layout_marginRight="27dp"
  46. android:text="@string/url">
  47. </TextView>
  48. <EditText
  49. android:id="@+id/edit_article_url"
  50. android:layout_width="fill_parent"
  51. android:layout_height="wrap_content">
  52. </EditText>
  53. </LinearLayout>
  54. <LinearLayout
  55. android:orientation="horizontal"
  56. android:layout_height="wrap_content"
  57. android:layout_width="match_parent"
  58. android:gravity="center"
  59. android:layout_marginTop="10dp">
  60. <Button
  61. android:id="@+id/button_modify"
  62. android:layout_width="wrap_content"
  63. android:layout_height="wrap_content"
  64. android:text="@string/modify">
  65. </Button>
  66. <Button
  67. android:id="@+id/button_delete"
  68. android:layout_width="wrap_content"
  69. android:layout_height="wrap_content"
  70. android:text="@string/delete">
  71. </Button>
  72. <Button
  73. android:id="@+id/button_add_article"
  74. android:layout_width="wrap_content"
  75. android:layout_height="wrap_content"
  76. android:paddingLeft="16dp"
  77. android:paddingRight="16dp"
  78. android:text="@string/add">
  79. </Button>
  80. <Button
  81. android:id="@+id/button_cancel"
  82. android:layout_width="wrap_content"
  83. android:layout_height="wrap_content"
  84. android:text="@string/cancel">
  85. </Button>
  86. </LinearLayout>
  87. </LinearLayout>
在res/drawable目录下,有一个border.xml文件定义了MainActivity界面上的ListView的背景,它的内容如下所示:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <shapexmlns:android="http://schemas.android.com/apk/res/android"
  3. android:shape="rectangle">
  4. <solidandroid:color="#ff0000ff"/>
  5. <strokeandroid:width="1dp"
  6. android:color="#000000">
  7. </stroke>
  8. <paddingandroid:left="7dp"
  9. android:top="7dp"
  10. android:right="7dp"
  11. android:bottom="7dp">
  12. </padding>
  13. <cornersandroid:radius="10dp"/>
  14. </shape>
程序使用到的字符串资源文件定义在res/values/strings.xml文件中,它的内容如下所示:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <resources>
  3. <stringname="app_name">Article</string>
  4. <stringname="article">Article</string>
  5. <stringname="add">Add</string>
  6. <stringname="modify">Modify</string>
  7. <stringname="delete">Delete</string>
  8. <stringname="title">Title:</string>
  9. <stringname="abs">Abstract:</string>
  10. <stringname="url">URL:</string>
  11. <stringname="ok">OK</string>
  12. <stringname="cancel">Cancel</string>
  13. </resources>
接下来再来看程序的配置文件AndroidManifest.xml:
view plain
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  3. package="shy.luo.article"
  4. android:versionCode="1"
  5. android:versionName="1.0">
  6. <applicationandroid:icon="@drawable/icon"android:label="@string/app_name">
  7. <activityandroid:name=".MainActivity"
  8. android:label="@string/app_name">
  9. <intent-filter>
  10. <actionandroid:name="android.intent.action.MAIN"/>
  11. <categoryandroid:name="android.intent.category.LAUNCHER"/>
  12. </intent-filter>
  13. </activity>
  14. <activity
  15. android:name=".ArticleActivity"
  16. android:label="@string/article">
  17. </activity>
  18. </application>
  19. </manifest>
编译脚本Android.mk的内容如下所示:
view plain
  1. LOCAL_PATH:=$(callmy-dir)
  2. include$(CLEAR_VARS)
  3. LOCAL_MODULE_TAGS:=optional
  4. LOCAL_STATIC_JAVA_LIBRARIES:=libArticlesProvider
  5. LOCAL_SRC_FILES:=$(callall-subdir-java-files)
  6. LOCAL_PACKAGE_NAME:=Article
  7. include$(BUILD_PACKAGE)
  8. ###################################################
  9. include$(CLEAR_VARS)
  10. LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES:=libArticlesProvider:./libs/ArticlesProvider.jar
  11. include$(BUILD_MULTI_PREBUILT)
这个编译脚本包含了两个部分的指令,一个是把libs目录下的预编译静态库ArticlesProvider.jar编译成一本地静态库libArticlesProvider,它的相关库文件保存在out/target/common/obj/JAVA_LIBRARIES/libArticlesProvider_intermediates目录下;另一个就是编译我们的程序Article了,它通过LOCAL_STATIC_JAVA_LIBRARIES变量来引用前面的libArticlesProvider库,这个库包含了所有我们用来访问ArticlesProvider这个Content Provider中的数据的常量。

下面我们就可以编译和打包这个应用程序了:

view plain
  1. USER-NAME@MACHINE-NAME:~/Android$mmmpackages/experimental/Article
  2. USER-NAME@MACHINE-NAME:~/Android$makesnod

这样,打包好的Android系统镜像文件system.img就包含我们这里所创建的Article应用程序了。

最后,就是运行模拟器来运行我们的例子了。关于如何在Android源代码工程中运行模拟器,请参考在Ubuntu上下载、编译和安装Android最新源代码一文。
执行以下命令启动模拟器:
view plain
  1. USER-NAME@MACHINE-NAME:~/Android$emulator
这个应用程序的主界面如下图所示:

点击下面的Add按钮,可以添加新的文章信息条目:


在前一个界面的文件列表中,点击某一个文章条目,便可以更新或者删除文章信息条目:



这样,Content Provider的使用实例就介绍完了。这篇文章的目的是使读者对Content Provider有一个大概的了解和感性的认识,在下一篇文章中,我们将详细介绍Article应用程序是如何获得ArticlesProvider这个ContentProvider接口的,只有获得了这个接口之后,Article应用程序才能访问ArticlesProvider的数据,敬请关注。

分享到:
评论

相关推荐

    Android应用开发详解

    Content Provider,讲述了Android不同应用程序之间相互共享数据的机制,包括ContentProvider和ContentResolver 第11章 Android中的多媒体应用 Android中的多媒体应用,讲述了Android的图片应用、音频及视频播放、...

    Android中使用Content Provider组件访问通讯录中的联系人和添加联系人案例详解

    Android中使用Content Provider组件访问通讯录中的联系人和添加联系人案例详解!

    《Android应用开发详解》源码_文档讲解

    《Android应用开发详解源码》目录第1章 Android概述 第2章 Android开发基础 第3章 Android中的资源访问 第4章 Android用户界面 第5章 Android基本程序单元Activity 第6章 Android组件之间的信使Intent 第7章 Android...

    android应用开发详解 郭宏志编著 pdf格式

    第六章 android组件之间的信使Intent 第七章 android Service组件 第八章 android广播事件处理Broadcast Receiver 第九章 android中的数据存取 第十章 Content Provider 第十一章 android中的多媒体应用 第十二章 ...

    《Android系统源代码情景分析》

    10.1 Content Provider组件应用实例 10.1.1 ArticlesProvider 10.1.2 Article 10.2 Content Provider组件的启动过程 10.3 Content Provider组件的数据共享原理 10.3.1 数据共享模型 10.3.2 数据传输...

    Android系统源代码情景分析-罗升阳-源码

    10.1 Content Provider组件应用实例 10.1.1 ArticlesProvider 10.1.2 Article 10.2 Content Provider组件的启动过程 10.3 Content Provider组件的数据共享原理 10.3.1 数据共享模型 10.3.2 数据传输过程 10.4...

    Android开发案例驱动教程 配套代码

    《Android开发案例驱动教程》 配套代码。 注: 由于第12,13,14章代码太大,无法上传到一个包中。 这三节代码会放到其他压缩包中。 作者:关东升,赵志荣 Java或C++程序员转变成为Android程序员 采用案例驱动模式...

    Android应用开发详解pdf版

    本书将android应用程序的四个基本组件:Activity、Service、Broadcast Receiver和Content Provider讲得很详细很清楚。每个组件都用了几个小的实例进行分析讲解,很容易看懂。 对Acitivit之间的Intent意图驱动机制也...

    Android 4.X手机/平板电脑程序设计入门、应用到精通_源代

     4大类Android程序完全详解,包括Activity、Service.Content provider和Broadcast receiver,以及App Widget程序; 2D和3D绘图程序架构与案例,奠定开发游戏程序的基础; Google地图与自动定位程序,让您学会开发...

    Android自学视频教程 part1

    第2篇为提高篇,主要包括Android高级组件的使用、Android中的事件处理、数据存储技术、Content Provider实现数据共享、图形图像处理技术、利用OpenGL实现3D图形、多媒体应用开发、线程与消息处理、网络编程技术和...

    Android开发与实践 pdf

    图像与动画处理技术,利用OpenGL实现3D图形,多媒体应用开发,Android数据存储技术,Content Provider实现数据共享,线程与消息处理,Service应用,网络通信技术,综合案例—家庭理财通,课程设计—猜猜鸡蛋放在哪只...

    《Google Android开发入门与实战》.pdf

    第6章 磨刀不误砍柴工——android应用程序结构介绍 57 6.1 android体系结构介绍 57 6.1.1 应用程序(application) 57 6.1.2 应用程序框架(application framework) 58 6.1.3 库(libraries)和运行环境...

    Android自学视频教程 part2

    第2篇为提高篇,主要包括Android高级组件的使用、Android中的事件处理、数据存储技术、Content Provider实现数据共享、图形图像处理技术、利用OpenGL实现3D图形、多媒体应用开发、线程与消息处理、网络编程技术和...

    [Android开发权威指南].李宁.扫描版.pdf

    第28章 Android综合案例六 笑脸连连看 游戏 "&gt;《Android开发权威指南》内容上涵盖了用最新的Android版本开发的大部分场景 全书分4个部分 分别从Android基础介绍 环境搭建 SDK介绍 到应用剖析 组件介绍 综合实例演示 ...

    新版Android开发教程.rar

    • 应用程序框架 支持组件的重用与替换 • Dalvik Dalvik Dalvik Dalvik 虚拟机 专为移动设备优化 • 集成的浏览器 基于开源的 WebKit 引擎 • 优化的图形库 包括定制的 2D 图形库, 3D 图形库基于 OpenGL ES 1.0 ...

    Google Android开发入门与实战的代码

    第6章 磨刀不误砍柴工——Android应用程序结构介绍 57 6.1 Android体系结构介绍 57 6.1.1 应用程序(Application) 57 6.1.2 应用程序框架(Application Framework) 58 6.1.3 库(Libraries)和运行环境...

    Android架构和应用

    介绍了Android应用的构成和工作机制,分别介绍了Activity、Intent、Receiver、Service、Content Provider的作用,详细分析了intent机制。 并通过实例,包括系统的需求分析,系统的主要功能,系统的总体业务描述和...

    Android编程中的四大基本组件与生命周期详解

    Android四大基本组件分别是Activity,Service服务,Content Provider内容提供者,BroadcastReceiver广播接收器。 一:了解四大基本组件 Activity : 应用程序中,一个Activity通常就是一个单独的屏幕,它上面可以显示...

Global site tag (gtag.js) - Google Analytics