Android开发之ContentProvider详解

 

Android中ContentProvider组件详解


ContentProvider(内容提供者)是Android中的四大组件之一。主要用于对外共享数据,也就是通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider对指定应用中的数据进行操作。 ContentProvider分为系统的和自定义的,系统的也就是例如联系人,图片等数据。

以下这段是Google Doc中对ContentProvider的大致概述。
内容提供者将一些特定的应用程序数据供给其它应用程序使用。数据可以存储于文件系统、SQLite数据库或其它方式。内容提供者继承于ContentProvider 基类,为其它应用程序取用和存储它管理的数据实现了一套标准方法。然而,应用程序并不直接调用这些方法,而是使用一个 ContentResolver 对象,调用它的方法作为替代。ContentResolver可以与任意内容提供者进行会话,与其合作来对所有相关交互通讯进行管理。

1.ContentProvider
Android提供了一些主要数据类型的ContentProvider,比如音频、视频、图片和私人通讯录等。可在android.provider包下面找到一些Android提供的ContentProvider。通过获得这些ContentProvider可以查询它们包含的数据,当然前提是已获得适当的读取权限。
主要方法:
public boolean onCreate() 在创建ContentProvider时调用
public Cursor query(Uri, String[], String, String[], String) 用于查询指定Uri的ContentProvider,返回一个Cursor
public Uri insert(Uri, ContentValues) 用于添加数据到指定Uri的ContentProvider中
public int update(Uri, ContentValues, String, String[]) 用于更新指定Uri的ContentProvider中的数据
public int delete(Uri, String, String[]) 用于从指定Uri的ContentProvider中删除数据
public String getType(Uri) 用于返回指定的Uri中的数据的MIME类型
*如果操作的数据属于集合类型,那么MIME类型字符串应该以vnd.android.cursor.dir/开头。
例如:要得到所有person记录的Uri为content://contacts/person,那么返回的MIME类型字符串为"vnd.android.cursor.dir/person"。
*如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头。
例如:要得到id为10的person记录的Uri为content://contacts/person/10,那么返回的MIME类型字符串应为"vnd.android.cursor.item/person"。

2.ContentResolver
当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用ContentResolver类来完成,要获取ContentResolver对象,可以使用Context提供的getContentResolver()方法。

[java]  view plain copy
  1. ContentResolver cr = getContentResolver();  
ContentResolver提供的方法和ContentProvider提供的方法对应的有以下几个方法。
public Uri  insert (Uri uri, ContentValues values) 用于添加数据到指定Uri的ContentProvider中。
public int  delete (Uri uri, String selection, String[] selectionArgs) 用于从指定Uri的ContentProvider中删除数据。
public int  update (Uri uri, ContentValues values, String selection, String[] selectionArgs) 用于更新指定Uri的ContentProvider中的数据。
public Cursor  query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) 用于查询指定Uri的ContentProvider。

 

3.Uri
Uri指定了将要操作的ContentProvider,其实可以把一个Uri看作是一个网址,我们把Uri分为三部分。
第一部分是"content://"。可以看作是网址中的"http://"。
第二部分是主机名或authority,用于唯一标识这个ContentProvider,外部应用需要根据这个标识来找到它。可以看作是网址中的主机名,比如"blog.csdn.net"。
第三部分是路径名,用来表示将要操作的数据。可以看作网址中细分的内容路径。

Android开发之ContentProvider详解_第1张图片

下面是用ContentProvider读取联系人数据,属于系统数据。完整代码下载:android_contentprovider_system.rar

注意:这里的联系人操作有点乱,关键是我还不是很熟,SDK1.6和SDK2.1的联系人操作很有很大不同,希望哪位大侠指点一下。

[java]  view plain copy
  1. /** 
  2.  * MainActivity 
  3.  *  
  4.  * @author zuolongsnail 
  5.  */  
  6. public class MainActivity extends Activity {  
  7.     private EditText nameET;  
  8.     private EditText numberET;  
  9.     private Button insertBtn;  
  10.     private Button deleteBtn;  
  11.     private Button queryBtn;  
  12.     private ListView contentView;  
  13.   
  14.     @Override  
  15.     public void onCreate(Bundle savedInstanceState) {  
  16.         super.onCreate(savedInstanceState);  
  17.         setContentView(R.layout.main);  
  18.         nameET = (EditText) findViewById(R.id.name);  
  19.         numberET = (EditText) findViewById(R.id.number);  
  20.         insertBtn = (Button) findViewById(R.id.insert);  
  21.         deleteBtn = (Button) findViewById(R.id.delete);  
  22.         queryBtn = (Button) findViewById(R.id.query);  
  23.         // 用于显示数据  
  24.         contentView = (ListView) findViewById(R.id.content);  
  25.         insertBtn.setOnClickListener(new OperateOnClickListener());  
  26.         deleteBtn.setOnClickListener(new OperateOnClickListener());  
  27.         queryBtn.setOnClickListener(new OperateOnClickListener());  
  28.     }  
  29.   
  30.     class OperateOnClickListener implements OnClickListener {  
  31.         @Override  
  32.         public void onClick(View v) {  
  33.             String name = nameET.getText().toString();  
  34.             String number = numberET.getText().toString();  
  35.             Person p = new Person(name, number);  
  36.             switch (v.getId()) {  
  37.             // 插入数据  
  38.             case R.id.insert:  
  39.                 insert(p);  
  40.                 view();  
  41.                 break;  
  42.             // 删除数据  
  43.             case R.id.delete:  
  44.                 delete(name);  
  45.                 view();  
  46.                 break;  
  47.             // 查询数据  
  48.             case R.id.query:  
  49.                 view();  
  50.                 break;  
  51.             }  
  52.         }  
  53.     }  
  54.   
  55.     // 显示数据  
  56.     private void view() {  
  57.         Cursor c = query("");  
  58.         ListAdapter listAdapter = new SimpleCursorAdapter(this, R.layout.list,  
  59.                 c, new String[] { People._ID, People.NAME, People.NUMBER },  
  60.                 new int[] { R.id.id, R.id.name, R.id.number });  
  61.         contentView.setAdapter(listAdapter);  
  62.     }  
  63.   
  64.     // 插入联系人  
  65.     private void insert(Person p) {  
  66.         // 获得ContentResolver对象  
  67.         ContentResolver cr = getContentResolver();  
  68.         ContentValues values = new ContentValues();  
  69.         values.put(People.NAME, p.name);  
  70.         // 表示是否把联系人添加到收藏(加星),1表示加入,0表示不加入,这行代码注释默认是不加入。  
  71.         values.put(Contacts.People.STARRED, 1);  
  72.         Uri uri = Contacts.People.createPersonInMyContactsGroup(cr, values);  
  73.         // 获得联系人People表的Uri  
  74.         Uri url = Uri.withAppendedPath(uri,  
  75.                 Contacts.People.Phones.CONTENT_DIRECTORY);  
  76.         values.clear();  
  77.         values.put(Contacts.Phones.TYPE, Contacts.Phones.NUMBER);  
  78.         values.put(Contacts.Phones.NUMBER, p.number);  
  79.         // 插入操作  
  80.         cr.insert(url, values);  
  81.     }  
  82.   
  83.     // 插入联系人  
  84.     private void delete(String name) {  
  85.         // 获得ContentResolver对象  
  86.         ContentResolver cr = getContentResolver();  
  87.         Uri url = Contacts.People.CONTENT_URI;  
  88.         // 设置删除条件  
  89.         String where = People.NAME + "=?";  
  90.         String[] selectionArgs = { name };  
  91.         cr.delete(url, where, selectionArgs);  
  92.     }  
  93.   
  94.     // 查询联系人  
  95.     private Cursor query(String name) {  
  96.         // 获得ContentResolver对象  
  97.         ContentResolver cr = getContentResolver();  
  98.         Uri uri = Contacts.People.CONTENT_URI;  
  99.         // 查询对象  
  100.         String[] projection = { People._ID, People.NAME, People.NUMBER };  
  101.         // 设置查询条件,这里我把selection和selectionArgs参数都设为null,表示查询全部数据  
  102.         String selection = null;  
  103.         String[] selectionArgs = null;  
  104.         if (!"".equals(name)) {  
  105.             selection = People.NAME + "=?";  
  106.             selectionArgs = new String[] { name };  
  107.         }  
  108.         // 设置排序条件  
  109.         String sortOrder = Contacts.People._ID;  
  110.         Cursor c = cr.query(uri, projection, selection, selectionArgs,  
  111.                 sortOrder);  
  112.         // if (c.moveToFirst()) {  
  113.         // for (int i = 0; i < c.getCount(); i++) {  
  114.         // c.moveToPosition(i);  
  115.         // String name = c.getString(c.getColumnIndexOrThrow(People.NAME));  
  116.         // String number = c.getString(c  
  117.         // .getColumnIndexOrThrow(People.NUMBER));  
  118.         // }  
  119.         // }  
  120.         return c;  
  121.     }  
  122. }  

程序截图: 

Android开发之ContentProvider详解_第2张图片


android ContentProvider和Uri详解  

一、使用ContentProvider(内容提供者)共享数据

ContentProvider在android中的作用是对外共享数据, 也就是说你可以通过ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider对你应用中的数据 进行添删改查。关于数据共享,以前我们学习过文件操作模式,知道通过指定文件的操作模式为Context.MODE_WORLD_READABLE或 Context.MODE_WORLD_WRITEABLE同样也可以对外共享数据。那么,这里为何要使用ContentProvider对外共享数据 呢?是这样的,如果采用文件操作模式对外共享数据,数据的访问方式会因数据存储的方式而不同,导致数据的访问方式无法统一,如:采用xml文件对外共享数 据,需要进行xml解析才能读取数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读取数据。
使用ContentProvider对外共享数据的好处是统一了数据的访问方式。
当应用需要通过ContentProvider对外共享数据时,第一步需要继承ContentProvider并重写下面方法:

public class PersonContentProvider extends ContentProvider{
   public boolean onCreate()
   public Uri insert(Uri uri, ContentValues values)
   public int delete(Uri uri, String selection, String[] selectionArgs)
   public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
   public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
   public String getType(Uri uri)
}

第二步需要在AndroidManifest.xml使用<provider>对该ContentProvider进行配置,为了能让 其他应用找到该ContentProvider ,ContentProvider采用了authorities(主机名/域名)对它进行唯一标识,你可以把ContentProvider看作是一个网 站(想想,网站也是提供数据者),authorities 就是他的域名:

<manifest.... >
   <application android:icon="@drawable/icon" android:label="@string/app_name">
      <provider android:name=".PersonContentProvider" 
           android:authorities="com.ljq.providers.personprovider"/>
   </application>
</manifest>

          

             

二、Uri介绍

Uri代表了要操作的数据,Uri主要包含了两部分信息:1》需要操作的ContentProvider ,2》对ContentProvider中的什么数据进行操作,一个Uri由以下几部分组成:          

Android开发之ContentProvider详解_第3张图片                 

ContentProvider(内容提供者)的scheme已经由Android所规定, scheme为:content://
主机名(或叫Authority)用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它
路径(path)可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下:
要操作person表中id为10的记录,可以构建这样的路径:/person/10
要操作person表中id为10的记录的name字段, person/10/name
要操作person表中的所有记录,可以构建这样的路径:/person
要操作xxx表中的记录,可以构建这样的路径:/xxx
当然要操作的数据不一定来自数据库,也可以是文件、xml或网络等其他存储方式,如下:
要操作xml文件中person节点下的name节点,可以构建这样的路径:/person/name
如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下:
Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person")

                

             

三、UriMatcher类使用介绍

因为Uri代表了要操作的数据,所以我们经常需要解析Uri,并从Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher和ContentUris 。掌握它们的使用,会便于我们的开发工作。
UriMatcher类用于匹配Uri,它的用法如下:
首先第一步把你需要匹配Uri路径全部给注册上,如下:

//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码
UriMatcher  sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配content://com.ljq.provider.personprovider/person路径,返回匹配码为1
sMatcher.addURI("com.ljq.provider.personprovider", "person", 1);//添加需要匹配uri,如果匹配就会返回匹配码
//如果match()方法匹配content://com.ljq.provider.personprovider/person/230路径,返回匹配码为2
sMatcher.addURI("com.ljq.provider.personprovider", "person/#", 2);//#号为通配符
switch (sMatcher.match(Uri.parse("content://com.ljq.provider.personprovider/person/10"))) { 
   case 1
     break;
   case 2
     break;
   default://不匹配
     break;
}

注册完需要匹配的Uri后,就可以使用sMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配就返回匹配码,匹配码是调用 addURI()方法传入的第三个参数,假设匹配content://com.ljq.provider.personprovider/person路 径,返回的匹配码为1 

              

                 

四、ContentUris类使用介绍

ContentUris类用于操作Uri路径后面的ID部分,它有两个比较实用的方法:
withAppendedId(uri, id)用于为路径加上ID部分:

Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person")
Uri resultUri = ContentUris.withAppendedId(uri, 10); 
//生成后的Uri为:content://com.ljq.provider.personprovider/person/10

parseId(uri)方法用于从路径中获取ID部分:

Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person/10")
long personid = ContentUris.parseId(uri);//获取的结果为:10

          

              

五、使用ContentProvider共享数据

ContentProvider类主要方法的作用:
public boolean onCreate():该方法在ContentProvider创建后就会被调用,Android开机后,ContentProvider在其它应用第一次访问它时才会被创建。
public Uri insert(Uri uri, ContentValues values):该方法用于供外部应用往ContentProvider添加数据。
public int delete(Uri uri, String selection, String[] selectionArgs):该方法用于供外部应用从ContentProvider删除数据。
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs):该方法用于供外部应用更新ContentProvider中的数据。
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder):该方法用于供外部应用从ContentProvider中获取数据。
public String getType(Uri uri):该方法用于返回当前Url所代表数据的MIME类型。

如果操作的数据属于集合类型,那么MIME类型字符串应该以vnd.android.cursor.dir/开头,

例如:要得到所有person记录的Uri为content://com.ljq.provider.personprovider/person,那么返回的MIME类型字符串应该为:"vnd.android.cursor.dir/person"。

如果要操作的数据属于非集合类型数据,那么MIME类型字符串应该以vnd.android.cursor.item/开头,

例如:得到id为10的person记录,Uri为content://com.ljq.provider.personprovider/person/10,那么返回的MIME类型字符串为:"vnd.android.cursor.item/person"。

              

                

六、使用ContentResolver操作ContentProvider中的数据

当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查 询操作时,可以使用ContentResolver 类来完成,要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。 ContentResolver 类提供了与ContentProvider类相同签名的四个方法:
public Uri insert(Uri uri, ContentValues values):该方法用于往ContentProvider添加数据。
public int delete(Uri uri, String selection, String[] selectionArgs):该方法用于从ContentProvider删除数据。
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs):该方法用于更新ContentProvider中的数据。
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder):该方法用于从ContentProvider中获取数据。

这些方法的第一个参数为Uri,代表要操作的ContentProvider和对其中的什么数据进行操作,

假设给定的是:Uri.parse("content://com.ljq.providers.personprovider/person /10"),那么将会对主机名为com.ljq.providers.personprovider的ContentProvider进行操作,操作的数 据为person表中id为10的记录。

使用ContentResolver对ContentProvider中的数据进行添加、删除、修改和查询操作:

ContentResolver resolver =  getContentResolver();
Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person");
//添加一条记录
ContentValues values = new ContentValues();
values.put("name", "linjiqin");
values.put("age", 25);
resolver.insert(uri, values);  
//获取person表中所有记录
Cursor cursor = resolver.query(uri, null, null, null, "personid desc");
while(cursor.moveToNext()){
   Log.i("ContentTest", "personid="+ cursor.getInt(0)+ ",name="+ cursor.getString(1));
}
//把id为1的记录的name字段值更改新为zhangsan
ContentValues updateValues = new ContentValues();
updateValues.put("name", "zhangsan");
Uri updateIdUri = ContentUris.withAppendedId(uri, 2);
resolver.update(updateIdUri, updateValues, null, null);
//删除id为2的记录
Uri deleteIdUri = ContentUris.withAppendedId(uri, 2);
resolver.delete(deleteIdUri, null, null);

            

               

七、监听ContentProvider中数据的变化

如果ContentProvider的访问者需要知道ContentProvider中的数据发生变化,可以在ContentProvider发生 数据变化时调用getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者,例子如下:

public class PersonContentProvider extends ContentProvider {
   public Uri insert(Uri uri, ContentValues values) {
      db.insert("person", "personid", values);
      getContext().getContentResolver().notifyChange(uri, null);
   }
}

如果ContentProvider的访问者需要得到数据变化通知,必须使用ContentObserver对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,系统就会调用ContentObserver的onChange()方法:

getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"),
       true, new PersonObserver(new Handler()));
public class PersonObserver extends ContentObserver{
   public PersonObserver(Handler handler) {
      super(handler);
   }
   public void onChange(boolean selfChange) {
      //此处可以进行相应的业务处理
   }
}


ContentProvider是怎么实现数据共享的呢?


下面先来了解一下这几个东东:
(1) URI
URI:统一资源标识符,代表要操作的数据,可以用来标识每个ContentProvider,这样你就可以通过指定的URI找到想要的ContentProvider,从中获取或修改数据。在Android中URI的格式如下图所示(图片来自于网络):

主要分三个部分:scheme, authority and path。scheme表示上图中的content://,authority表示B部分,path表示C和D部分。
A部分:表示是一个Android内容URI,说明由ContentProvider控制数据,该部分是固定形式,不可更改的。
B部分:是URI的授权部分,是唯一标识符,用来定位ContentProvider。格式一般是自定义ContentProvider类的完全限定名称,注册时需要用到,如:com.alexzhou.provider.NoteProvider
C部分和D部分:是每个ContentProvider内部的路径部分,C和D部分称为路径片段,C部分指向一个对象集合,一般用表的名字,如:/notes表示一个笔记集合;D部分指向特定的记录,如:/notes/1表示id为1的笔记,如果没有指定D部分,则返回全部记录。
在开发中通常使用常量来定义URI,如下面的CONTENT_URI:

1
2
public static final String AUTHORITY = "com.alexzhou.provider.NoteProvider" ;
public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY + "/notes" );

(2) MIME
MIME是指定某个扩展名的文件用一种应用程序来打开,就像你用浏览器查看PDF格式的文件,浏览器会选择合适的应用来打开一样。Android中的工作方式跟HTTP类似,ContentProvider会根据URI来返回MIME类型,ContentProvider会返回一个包含两部分的字符串。MIME类型一般包含两部分,如:
text/html
text/css
text/xml
application/pdf
等,分为类型和子类型,Android遵循类似的约定来定义MIME类型,每个内容类型的Android MIME类型有两种形式:多条记录(集合)和单条记录。
多条记录
vnd.android.cursor.dir/vnd.alexzhou.note
单条记录
vnd.android.cursor.item/vnd.alexzhou.note
vnd表示这些类型和子类型具有非标准的、供应商特定的形式。Android中类型已经固定好了,不能更改,只能区别是集合还是单条具体记录,子类型vnd.之后的内容可以按照格式随便填写。在使用Intent时,会用到MIME这玩意,根据Mimetype打开符合条件的活动。

3. 接下来通过一个简单的demo,来学习怎么创建自定义的ContentProvider,这里数据源选用SQLite,最常用的也是这个。
(1) 创建一个类NoteContentProvider,继承ContentProvider,需要实现下面5个方法:
query
insert
update
delete
getType

这些方法是eclipse自动生成的,暂时先不动他们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
author:alexzhou
date  :2013-2-25
  **/
 
public class NoteContentProvider extends ContentProvider {
 
     @Override
     public boolean onCreate() {
         // TODO Auto-generated method stub
         return false ;
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
         // TODO Auto-generated method stub
         return null ;
     }
 
     @Override
     public String getType(Uri uri) {
         // TODO Auto-generated method stub
         return null ;
     }
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
         // TODO Auto-generated method stub
         return null ;
     }
 
     @Override
     public int delete(Uri uri, String selection, String[] selectionArgs) {
         // TODO Auto-generated method stub
         return 0 ;
     }
 
     @Override
     public int update(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         // TODO Auto-generated method stub
         return 0 ;
     }
 
}

(2)先来设计一个数据库,用来存储笔记信息,主要包含_ID,title,content,create_date四个字段。创建NoteProviderMetaData类,封装URI和数据库、表、字段相关信息,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
author:alexzhou
date  :2013-2-25
  **/
 
public class NoteProviderMetaData {
 
     public static final String AUTHORITY = "com.alexzhou.provider.NoteProvider" ;
 
     public static final String DATABASE_NAME = "note.db" ;
     public static final int DATABASE_VERSION = 1 ;
 
     /**
      *
      *数据库中表相关的元数据
      */
     public static final class NoteTableMetaData implements BaseColumns {
 
         public static final String TABLE_NAME = "notes" ;
         public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY + "/notes" );
         public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.alexzhou.note" ;
         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.alexzhou.note" ;
 
         public static final String NOTE_TITLE = "title" ;
         public static final String NOTE_CONTENT = "content" ;
         public static final String CREATE_DATE = "create_date" ;
 
         public static final String DEFAULT_ORDERBY = "create_date DESC" ;
 
         public static final String SQL_CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " ("
                                         + _ID + " INTEGER PRIMARY KEY,"
                                         + NOTE_TITLE + " VARCHAR(50),"
                                         + NOTE_CONTENT + " TEXT,"
                                         + CREATE_DATE + " INTEGER"
                                         + ");" ;
     }
}

AUTHORITY代表授权,该字符串和在Android描述文件AndroidManifest.xml中注册该ContentProvider时的android:authorities值一样,NoteTableMetaData继承BaseColumns,后者提供了标准的_id字段,表示行ID。
(3) ContentProvider是根据URI来获取数据的,那它怎么区分不同的URI呢,因为无论是获取笔记列表还是获取一条笔记都是调用query方法,现在来实现这个功能。需要用到类UriMatcher,该类可以帮助我们识别URI类型,下面看实现源码:

1
2
3
4
5
6
7
8
9
private static final UriMatcher sUriMatcher;
private static final int COLLECTION_INDICATOR = 1 ;
private static final int SINGLE_INDICATOR = 2 ;
 
static {
     sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
     sUriMatcher.addURI(NoteProviderMetaData.AUTHORITY, "notes" , COLLECTION_INDICATOR);
     sUriMatcher.addURI(NoteProviderMetaData.AUTHORITY, "notes/#" , SINGLE_INDICATOR);
}

这段代码是NoteContentProvider类中的,UriMatcher的工作原理:首先需要在UriMatcher中注册URI模式,每一个模式跟一个唯一的编号关联,注册之后,在使用中就可以根据URI得到对应的编号,当模式不匹配时,UriMatcher将返回一个NO_MATCH常量,这样就可以区分了。
(4) 还需为查询设置一个投影映射,主要是将抽象字段映射到数据库中真实的字段名称,因为这些字段有时是不同的名称,既抽象字段的值可以不跟数据库中的字段名称一样。这里使用HashMap来完成,key是抽象字段名称,value对应数据库中的字段名称,不过这里我把两者的值设置是一样的,在NoteContentProvider.java中添加如下代码。

1
2
3
4
5
6
7
8
private static HashMap<String, String> sNotesProjectionMap;
static {
     sNotesProjectionMap = new HashMap<String, String>();
     sNotesProjectionMap.put(NoteProviderMetaData.NoteTableMetaData._ID, NoteProviderMetaData.NoteTableMetaData._ID);
     sNotesProjectionMap.put(NoteProviderMetaData.NoteTableMetaData.NOTE_CONTENT, NoteProviderMetaData.NoteTableMetaData.NOTE_CONTENT);
     sNotesProjectionMap.put(NoteProviderMetaData.NoteTableMetaData.NOTE_TITLE, NoteProviderMetaData.NoteTableMetaData.NOTE_TITLE);
     sNotesProjectionMap.put(NoteProviderMetaData.NoteTableMetaData.CREATE_DATE, NoteProviderMetaData.NoteTableMetaData.CREATE_DATE);
}

(5) 在NoteContentProvider.java中创建一个内部类DatabaseHelper,继承自SQLiteOpenHelper,完成数据库表的创建、更新,这样可以通过它获得数据库对象,相关代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private DatabaseHelper mDbHelper;
 
private static class DatabaseHelper extends SQLiteOpenHelper {
 
     public DatabaseHelper(Context context) {
         super (context, NoteProviderMetaData.DATABASE_NAME, null , NoteProviderMetaData.DATABASE_VERSION);
     }
 
     @Override
     public void onCreate(SQLiteDatabase db) {
         Log.e( "DatabaseHelper" , "create table: " + NoteProviderMetaData.NoteTableMetaData.SQL_CREATE_TABLE);
         db.execSQL(NoteProviderMetaData.NoteTableMetaData.SQL_CREATE_TABLE);
     }
 
     @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
         db.execSQL( "DROP TABLE IF EXISTS " + NoteProviderMetaData.NoteTableMetaData.TABLE_NAME);
         onCreate(db);
     }
 
}

(6) 现在来分别实现第一步中未实现的5个方法,先来实现query方法,这里借助SQLiteQueryBuilder来为查询设置投影映射以及设置相关查询条件,看源码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public Cursor query(Uri uri, String[] projection, String selection,
         String[] selectionArgs, String sortOrder) {
     SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
     switch (sUriMatcher.match(uri)) {
     case COLLECTION_INDICATOR:
         // 设置查询的表
         queryBuilder.setTables(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME);
         // 设置投影映射
         queryBuilder.setProjectionMap(sNotesProjectionMap);
         break ;
 
     case SINGLE_INDICATOR:
         queryBuilder.setTables(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME);
         queryBuilder.setProjectionMap(sNotesProjectionMap);
         queryBuilder.appendWhere(NoteProviderMetaData.NoteTableMetaData._ID + "=" + uri.getPathSegments().get( 1 ));
         break ;
 
     default :
         throw new IllegalArgumentException( "Unknow URI: " + uri);
     }
 
     String orderBy;
     if (TextUtils.isEmpty(sortOrder))
     {
         orderBy = NoteProviderMetaData.NoteTableMetaData.DEFAULT_ORDERBY;
     } else {
         orderBy = sortOrder;
     }
     SQLiteDatabase db = mDbHelper.getReadableDatabase();
     Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null , null , orderBy);
 
     return cursor;
}

返回的是一个Cursor对象,它是一个行集合,包含0和多个记录,类似于JDBC中的ResultSet,可以前后移动游标,得到每行每列中的数据。注意的是,使用它需要调用moveToFirst(),因为游标默认是在第一行之前。
(7)实现insert方法,实现把记录插入到基础数据库中,然后返回新创建的记录的URI。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Uri insert(Uri uri, ContentValues values) {       
     if (sUriMatcher.match(uri) != COLLECTION_INDICATOR) {
         throw new IllegalArgumentException( "Unknown URI " + uri);
     }
 
     SQLiteDatabase db = mDbHelper.getWritableDatabase();
     long rowID = db.insert(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, null , values);
 
     if (rowID > 0 ) {
         Uri retUri = ContentUris.withAppendedId(NoteProviderMetaData.NoteTableMetaData.CONTENT_URI, rowID);
         return retUri;
     }
 
     return null ;
}

(8) 实现update方法,根据传入的列值和where字句来更新记录,返回更新的记录数,看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public int update(Uri uri, ContentValues values, String selection,
         String[] selectionArgs) {
     SQLiteDatabase db = mDbHelper.getWritableDatabase();
     int count = - 1 ;
     switch (sUriMatcher.match(uri)) {
     case COLLECTION_INDICATOR:
         count = db.update(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, values, null , null );
         break ;
 
     case SINGLE_INDICATOR:
         String rowID = uri.getPathSegments().get( 1 );
         count = db.update(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, values, NoteProviderMetaData.NoteTableMetaData._ID + "=" + rowID, null );
         break ;
 
     default :
         throw new IllegalArgumentException( "Unknow URI : " + uri);
 
     }
     this .getContext().getContentResolver().notifyChange(uri, null );
     return count;
}

notifyChange函数是在更新数据时,通知其他监听对象。
(9)实现delete方法,该方法返回删除的记录数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int delete(Uri uri, String selection, String[] selectionArgs) {
     SQLiteDatabase db = mDbHelper.getWritableDatabase();
     int count = - 1 ;
     switch (sUriMatcher.match(uri)) {
     case COLLECTION_INDICATOR:
         count = db.delete(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, selection, selectionArgs);
         break ;
 
     case SINGLE_INDICATOR:
         String rowID = uri.getPathSegments().get( 1 );
         count = db.delete(NoteProviderMetaData.NoteTableMetaData.TABLE_NAME, NoteProviderMetaData.NoteTableMetaData._ID + "=" + rowID, null );
         break ;
 
     default :
         throw new IllegalArgumentException( "Unknow URI :" + uri);
 
     }
     // 更新数据时,通知其他ContentObserver
     this .getContext().getContentResolver().notifyChange(uri, null );
     return count;
}

(10) 实现getType方法,根据URI返回MIME类型,这里主要用来区分URI是获取集合还是单条记录,这个方法在这里暂时没啥用处,在使用Intent时有用。

1
2
3
4
5
6
7
8
9
10
11
12
public String getType(Uri uri) {
     switch (sUriMatcher.match(uri)) {
         case COLLECTION_INDICATOR:
             return NoteProviderMetaData.NoteTableMetaData.CONTENT_TYPE;
 
         case SINGLE_INDICATOR:
             return NoteProviderMetaData.NoteTableMetaData.CONTENT_ITEM_TYPE;
 
         default :
             throw new IllegalArgumentException( "Unknow URI: " + uri);
     }
}

(11) 在AndroidManifest.xml中注册该ContentProvider,这样系统才找得到,当然你也可以设置相关的权限,这里就不设置了。

1
< provider android:name = "NoteContentProvider" android:authorities = "com.alexzhou.provider.NoteProvider" />

到现在为止,自定义ContentProvider的全部代码已经完成,下面创建一个简单的应用来测试一下。
主要测试insert、update、delete、query这四个函数。

1. 现在数据库中没有数据,所以先测试insert,插入一条数据。主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static final String AUTHORITY = "com.alexzhou.provider.NoteProvider" ;
public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY + "/notes" );
 
private void insert() {
      ContentValues values = new ContentValues();
      values.put( "title" , "hello" );
      values.put( "content" , "my name is alex zhou" );
      long time = System.currentTimeMillis();
      values.put( "create_date" , time);
      Uri uri = this .getContentResolver().insert(CONTENT_URI, values);
      Log.e( "test " , uri.toString());
     }

这里需要获得CONTENT_URI值,其他的应用可以通过ContentResolver接口访问ContentProvider提供的数据。logcat的输出如下:
因为现在数据库和表还不存在,所以首先会创建表,返回的URI的值是第一条记录的URI。
2. 测试query方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void query() {
     Cursor cursor = this .getContentResolver().query(CONTENT_URI, null , null , null , null );
     Log.e( "test " , "count=" + cursor.getCount());
     cursor.moveToFirst();
     while (!cursor.isAfterLast()) {
         String title = cursor.getString(cursor.getColumnIndex( "title" ));
         String content = cursor.getString(cursor.getColumnIndex( "content" ));
         long createDate = cursor.getLong(cursor.getColumnIndex( "create_date" ));
         Log.e( "test " , "title: " + title);
         Log.e( "test " , "content: " + content);
         Log.e( "test " , "date: " + createDate);
 
         cursor.moveToNext();
     }
     cursor.close();
}

logcat输入如下,正是刚才插入的值。
Android开发之ContentProvider详解_第4张图片
3. 测试update方法

1
2
3
4
5
6
7
private void update() {
     ContentValues values = new ContentValues();
     values.put( "content" , "my name is alex zhou !!!!!!!!!!!!!!!!!!!!!!!!!!" );
     int count = this .getContentResolver().update(CONTENT_URI, values, "_id=1" , null );
     Log.e( "test " , "count=" +count);
     query();
}

logcat输出:

刚才插入的值已被更新了。
(4) 测试delete方法

1
2
3
4
5
private void delete() {
     int count = this .getContentResolver().delete(CONTENT_URI, "_id=1" , null );
     Log.e( "test " , "count=" +count);
     query();
}

看logcat输出:

再次查询时,已经没有了数据。



ContentProvider的内部原理


  自定义一个ContentProvider,来实现内部原理
  步骤:
  1、定义一个CONTENT_URI常量(里面的字符串必须是唯一)
  Public static final Uri CONTENT_URI = Uri.parse("content://com.WangWeiDa.MyContentprovider");
  如果有子表,URI为:
  Public static final Uri CONTENT_URI = Uri.parse("content://com.WangWeiDa.MyContentProvider/users");
  2、定义一个类,继承ContentProvider
  Public class MyContentProvider extends ContentProvider
  3、实现ContentProvider的所有方法(query、insert、update、delete、getType、onCreate)
  package com.WangWeiDa.cp;
  
  import java.util.HashMap;
  
  import com.WangWeiDa.cp.MyContentProviderMetaData.UserTableMetaData;
  import com.WangWeiDa.data.DatabaseHelp;
  
  import android.content.ContentProvider;
  import android.content.ContentUris;
  import android.content.ContentValues;
  import android.content.UriMatcher;
  import android.database.Cursor;
  import android.database.sqlite.SQLiteDatabase;
  import android.database.sqlite.SQLiteQueryBuilder;
  import android.net.Uri;
  import android.text.TextUtils;
  
  public class MyContentProvider extends ContentProvider {
   //访问表的所有列
   public static final int INCOMING_USER_COLLECTION = 1;
   //访问单独的列
   public static final int INCOMING_USER_SINGLE = 2;
   //操作URI的类
   public static final UriMatcher uriMatcher;
   //为UriMatcher添加自定义的URI
   static{
   uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
   uriMatcher.addURI(MyContentProviderMetaData.AUTHORITIES,"/user",
   INCOMING_USER_COLLECTION);
   uriMatcher.addURI(MyContentProviderMetaData.AUTHORITIES,"/user/#",
   INCOMING_USER_SINGLE);
  
   }
   private DatabaseHelp dh;
   //为数据库表字段起别名
   public static HashMap userProjectionMap;
   static
   {
   userProjectionMap = new HashMap();
   userProjectionMap.put(UserTableMetaData._ID,UserTableMetaData._ID);
   userProjectionMap.put(UserTableMetaData.USER_NAME, UserTableMetaData.USER_NAME);
   }
   /**
   * 删除表数据
   */
   @Override
   public int delete(Uri uri, String selection, String[] selectionArgs) {
   System.out.println("delete");
   //得到一个可写的数据库
   SQLiteDatabase db = dh.getWritableDatabase();
   //执行删除,得到删除的行数
   int count = db.delete(UserTableMetaData.TABLE_NAME, selection, selectionArgs);
   return count;
   }
   /**
   * 数据库访问类型
   */
   @Override
   public String getType(Uri uri) {
   System.out.println("getType");
   //根据用户请求,得到数据类型
   switch (uriMatcher.match(uri)) {
   case INCOMING_USER_COLLECTION:
   return MyContentProviderMetaData.UserTableMetaData.CONTENT_TYPE;
   case INCOMING_USER_SINGLE:
   return MyContentProviderMetaData.UserTableMetaData.CONTENT_TYPE_ITEM;
   default:
   throw new IllegalArgumentException("UnKnown URI"+uri);
   }
   }
   /**
   * 插入数据
   */
   @Override
   public Uri insert(Uri uri, ContentValues values) {
   //得到一个可写的数据库
   SQLiteDatabase db = dh.getWritableDatabase();
   //向指定的表插入数据,得到返回的Id
   long rowId = db.insert(UserTableMetaData.TABLE_NAME, null, values);
   if(rowId > 0){//判断插入是否执行成功
   //如果添加成功,利用新添加的Id和
   Uri insertedUserUri = ContentUris.withAppendedId(UserTableMetaData.CONTENT_URI, rowId);
   //通知监听器,数据已经改变
   getContext().getContentResolver().notifyChange(insertedUserUri, null);
   return insertedUserUri;
   }
   return uri;
   }
   /**
   * 创建ContentProvider时调用的回调函数
   */
   @Override
   public boolean onCreate() {
   System.out.println("onCreate");
   //得到数据库帮助类
   dh = new DatabaseHelp(getContext(),MyContentProviderMetaData.DATABASE_NAME);
   return false;
   }
   /**
   * 查询数据库
   */
   @Override
   public Cursor query(Uri uri, String[] projection, String selection,
   String[] selectionArgs, String sortOrder) {
   //创建一个执行查询的Sqlite
   SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   //判断用户请求,查询所有还是单个
   switch(uriMatcher.match(uri)){
   case INCOMING_USER_COLLECTION:
   //设置要查询的表名
   qb.setTables(UserTableMetaData.TABLE_NAME);
   //设置表字段的别名
   qb.setProjectionMap(userProjectionMap);
   break;
   case INCOMING_USER_SINGLE:
   qb.setTables(UserTableMetaData.TABLE_NAME);
   qb.setProjectionMap(userProjectionMap);
   //追加条件,getPathSegments()得到用户请求的Uri地址截取的数组,get(1)得到去掉地址中/以后的第二个元素
   qb.appendWhere(UserTableMetaData._ID + "=" + uri.getPathSegments().get(1));
   break;
   }
   //设置排序
   String orderBy;
   if(TextUtils.isEmpty(sortOrder)){
   orderBy = UserTableMetaData.DEFAULT_SORT_ORDER;
   }
   else{
   orderBy = sortOrder;
   }
   //得到一个可读的数据库
   SQLiteDatabase db = dh.getReadableDatabase();
   //执行查询,把输入传入
   Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
   //设置监听
   c.setNotificationUri(getContext().getContentResolver(), uri);
   return c;
   }
   /**
   * 更新数据库
   */
   @Override
   public int update(Uri uri, ContentValues values, String selection,
   String[] selectionArgs) {
   System.out.println("update");
   //得到一个可写的数据库
   SQLiteDatabase db = dh.getWritableDatabase();
   //执行更新语句,得到更新的条数
   int count = db.update(UserTableMetaData.TABLE_NAME, values, selection, selectionArgs);
   return count;
   }
  }

  4、在AndroidMinifest.xml中进行声明
   android:name=".cp.MyContentProvider" android:authorities="com.WangWeiDa.cp.MyContentProvider" />

  **为ContentProvider提供一个常量类MyContentProviderMetaData.java
  package com.WangWeiDa.cp; 
  import android.net.Uri;
  import android.provider.BaseColumns;
  
  public class MyContentProviderMetaData {
   //URI的指定,此处的字符串必须和声明的authorities一致
   public static final String AUTHORITIES = "com.wangweida.cp.MyContentProvider";
   //数据库名称
   public static final String DATABASE_NAME = "myContentProvider.db";
   //数据库的版本
   public static final int DATABASE_VERSION = 1;
   //表名 
   public static final String USERS_TABLE_NAME = "user"; 
   public static final class UserTableMetaData implements BaseColumns{
   //表名
   public static final String TABLE_NAME = "user";
   //访问该ContentProvider的URI
   public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITIES + "/user");
   //该ContentProvider所返回的数据类型的定义
   public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.myprovider.user";
   public static final String CONTENT_TYPE_ITEM = "vnd.android.cursor.item/vnd.myprovider.user";
   //列名
   public static final String USER_NAME = "name";
   //默认的排序方法
   public static final String DEFAULT_SORT_ORDER = "_id desc";
   }
  }


Android之ContentProvider总结

1.适用场景

1) ContentProvider为存储和读取数据提供了统一的接口

2) 使用ContentProvider,应用程序可以实现数据共享

3) android内置的许多数据都是使用ContentProvider形式,供开发者调用的(如视频,音频,图片,通讯录等)

2.相关概念介绍

1)ContentProvider简介
       当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。

2)Uri类简介

      Uri uri = Uri.parse("content://com.changcheng.provider.contactprovider/contact")

      在Content Provider中使用的查询字符串有别于标准的SQL查询。很多诸如select, add, delete, modify等操作我们都使用一种特殊的URI来进行,这种URI由3个部分组成, “content://”, 代表数据的路径,和一个可选的标识数据的ID。以下是一些示例URI:

     content://media/internal/images  这个URI将返回设备上存储的所有图片
     content://contacts/people/  这个URI将返回设备上的所有联系人信息
     content://contacts/people/45 这个URI返回单个结果(联系人信息中ID为45的联系人记录)

  尽管这种查询字符串格式很常见,但是它看起来还是有点令人迷惑。为此,Android提供一系列的帮助类(在android.provider包下),里面包含了很多以类变量形式给出的查询字符串,这种方式更容易让我们理解一点,因此,如上面content://contacts/people/45这个URI就可以写成如下形式:

  Uri person = ContentUris.withAppendedId(People.CONTENT_URI,  45);

然后执行数据查询:

Cursor cur = managedQuery(person, null, null, null);

这个查询返回一个包含所有数据字段的游标,我们可以通过迭代这个游标来获取所有的数据:

复制代码
package com.wissen.testApp;
public class ContentProviderDemo extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
       displayRecords();
    }

    private void displayRecords() {
        //

你可能感兴趣的:(Android开发,数据存储,android应用)