zoukankan      html  css  js  c++  java
  • 【Android】ContentProvider

    转载地址:http://www.cnblogs.com/lqminn/archive/2012/10/16/2725624.html

    一、ContentProvider的概念   ContentProvider:为存储和获取数据提供统一的接口。可以在不同的应用程序之间共享数据。可支持在多个应用中存储和读取数据。这也是跨应用共享数据的唯一方式。在android系统中,没有一个公共的内存区域,供多个应用共享存储数据。Android提供了一些主要数据类型的Contentprovider,比如音频、视频、图片和私人通讯录等。可在android.provider包下面找到一些android提供的Contentprovider。可以获得这些Contentprovider,查询它们包含的数据,当然前提是已获得适当的读取权限。

    如果想公开自己的数据,那么可有两种办法:

    1)创建自己的 Content provider ,需要继承ContentProvider类;

    2)如果你的数据和已存在的 Content provider 数据结构一致,可以将数据写到已存在的 Content provider 中,当然前提是获取写该 Content provider 的权限。比如把OA中的成员通讯信息加入到系统的联系人 Content provider 中。

    1、ContentProvider使用表的形式来组织数据

    无论数据的来源是什么,ContentProvider都会认为是一种表,然后把数据组织成表格

    2、ContentProvider提供的方法

    query:查询
    insert:插入
    update:更新
    delete:删除 
    getType:得到数据类型
    onCreate:创建数据时调用的回调函数
    

    3、每个ContentProvider都有一个公共的URI,这个URI用于表示这个ContentProvider所提供的数据。Android所提供的ContentProvider都存放在android.provider包当中。

    4.数据模型

    Contentprovider展示数据类似一个单个数据库表。其中:

    每行有个带唯一值的数字字段,名为_ID,可用于对表中指定记录的定位; Contentprovider 返回的数据结构,是类似JDBC的ResultSet,在android中,是Cursor对象。

    5.URI

    每个contentprovider定义一个唯一的公开的URI,用于指定到它的数据集。一个contentprovider可以包含多个数据集(可以看作多张表),这样,就需要有多个URI与每个数据集对应(可以看作是数据库)。这些URI要以这样的格式开头:

    content://
    

    表示这个URI指定一个contentprovider如果你想创建自己的contentprovider,最好把自定义的URI设置为类的常量,这样简化别人的调用,并且以后如果更新URI也很容易。android定义了CONTENT_URI常量用于URI,比如:

    android.provider.Contacts.Phones.CONTENT_URI
    android.provider.Contacts.Photos.CONTENT_URI
    

    查询 Content provider

    要想使用一个contentprovider,需要以下信息:

    1)定义这个 content provider 的URI 返回结果的字段名称 这些字段的数据类型

    2)如果需要查询contentprovider数据集的特定记录(行),还需要知道该记录的ID的值。

    构建查询

    查询就是输入URI等参数,其中URI是必须的,其他是可选的,如果系统能找到URI对应的contentprovider将返回一个Cursor对象。

    可以通过ContentResolver.query()或者Activity.managedQuery()方法。两者的方法参数完全一样,查询过程和返回值也是相同的。区别是,通过Activity.managedQuery()方法,不但获取到Cursor对象,而且能够管理Cursor对象的生命周期,比如当Activity暂停(pause)的时候,卸载该Cursor对象,当Activity restart的时候重新查询。另外,也可以对一个没有处于Activity管理的Cursor对象做成被Activity管理的,通过调用 Activity.startManaginCursor()方法。

    类似这样:

    Cursor cur = managedQuery(myPerson, null, null, null, null);//其中第一个参数myPerson是Uri类型实例。
    

     如果需要查询的是指定行的记录,需要用_ID值,比如ID值为23,URI将是类似:

    content://. . . ./23
    

    android提供了方便的方法,让开发者不需要自己拼接上面这样的URI,比如类似:

     Uri myPerson = ContentUris.withAppendedId(People.CONTENT_URI, 23);

     或者:

    Uri myPerson = Uri.withAppendedPath(People.CONTENT_URI, "23");

    二者的区别是一个接收整数类型的ID值,一个接收字符串类型。

    6.其他几个参数:(从这边可以看出非常类似于数据库)

    1)names,可以为null,表示取数据集的全部列,或者声明一个String数组,数组中存放列名称,比如: People._ID 。一般列名都在该 Content provider 中有常量对应;

    2)针对返回结果的过滤器,格式类似于SQL中的WHERE子句,区别是不带WHERE关键字,如果返回null表示不过滤,比如 name=? 

    3)前面过滤器的参数,是String数组,是针对前面条件中?占位符的值;

    4)排序参数,类似SQL的ORDER BY字句,不过不需要写ORDER BY部分,比如 name desc ,如果不排序,可输入null。

    下面实例适用于android 2.0及以上版本,从android通讯录中得到姓名字段:

    Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);

    返回结果为:

    返回值的内容类似上图,不同的contentprovider会有不同的列和名称,但是会有两个相同的列,上面提到过的一个是_ID,用于唯一标识记录,还有一个_COUNT,用于记录整个结果集的大小,可以看到上面图中的_COUNT的值是相同的。

    读取返回的数据

    如果在查询的时候使用到ID,那么返回的数据只有一条记录。在其他情况下,一般会有多条记录。和JDBC的ResultSet类似,需要操作游标遍历结果集,在每行,再通过列名获取到列的值,可以通过getString()、getInt()、getFloat()等方法获取值。比如类似下面:

     

    while (cursor.moveToNext()) {
        builder
                .append(
                        cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)))
                .append("-");
    }

    从这路也可以看出其实是非常相似的!(区别:和JDBC中不同,没有直接通过列名获取列值的方法,只能先列名获取到列的整型索引值,然后再通过该索引值定位获取列的值。(SQLite数据库也是如此)

    二、编辑数据

    可以通过contentprovider实现以下编辑功能:

    1)增加新的记录;

    2)在已经存在的记录中增加新的值;

    3)批量更新已经存在的多个记录;

    4)删除记录。

    所有的编辑功能都是通过ContentResolver的方法实现。一些Contentprovider对权限要求更严格一些,需要写的权限,如果没有会报错。

    a)增加记录

    要想增加记录到contentprovider,首先,要在ContentValues对象中设置类似map的键值对,在这里,键的值对应contentprovider中的列的名字,键值对的值,是对应列希望的类型。然后,调用ContentResolver.insert()方法,传入这个ContentValues对象,和对应Contentprovider的URI即可。返回值是这个新记录的URI对象。这样你可以通过这个URI获得包含这条记录的Cursor对象。比如:

    ContentValues values = new ContentValues();
    values.put(People.NAME, "Abraham Lincoln"); Uri uri = getContentResolver().insert(People.CONTENT_URI, values);//如何通过Uri获取这条记录?

    b)在原有记录上增加值

    如果记录已经存在,可在记录上增加新的值,或者编辑已经存在的值。首先要过去到原来的值对象,然后要清除原有的值,然后像上面增加记录一样即可

    Uri uri=Uri.withAppendedPath(People.CONTENT_URI, "23");//获取特定的行的uri
    Uri phoneUri = Uri.withAppendedPath(uri, People.Phones.CONTENT_DIRECTORY);//获取特定的数据列
    
    values.clear();
    values.put(People.Phones.TYPE, People.Phones.TYPE_MOBILE);
    values.put(People.Phones.NUMBER, "1233214567");
    getContentResolver().insert(phoneUri, values);
    

    (这个例子貌似揭示了联系人的联系方式也是使用一个表来存储的,也就是说,表中有表)

    c)批量更新值

    批量更新一组记录的值,比如NY改名为Eew York。可调用ContenResolver.update()方法。

    d)删除记录

    如果是删除单个记录,调用ContentResolver.delete()方法,URI参数,指定到具体行即可。

    如果是删除多个记录,调用ContentResolver.delete()方法,URI参数指定Contentprovider即可,并带一个类似SQL的WHERE子句条件。这里和上面类似,不带WHERE关键字。

    三、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)
    4、在AndroidMainfest.xml中申明

    创建contentprovider,需要:设置存储系统。大多数 content provider 使用文件或者SQLite数据库,不过你可以用任何方式存储数据。android提供SQLiteoOpenHelper帮助开发者创建和管理SQLiteDatabase。 继承ContentProvider,提供对数据的访问。 在manifest文件中声明content provider 。也就是遵循以下几步:

    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)
    4、在AndroidMainfest.xml中申明

    创建contentprovider,需要:设置存储系统。大多数 content provider 使用文件或者SQLite数据库,不过你可以用任何方式存储数据。android提供SQLiteoOpenHelper帮助开发者创建和管理SQLiteDatabase。 继承ContentProvider,提供对数据的访问。 在manifest文件中声明content provider 。也就是遵循以下几步:

    a. 创建一个继承了ContentProvider父类的类
    b. 定义一个名为CONTENT_URI,并且是public static final的Uri类型的类变量,你必须为其指定一个唯一的字符串值,最好的方案是以类的全名称, 如:
    public static final Uri CONTENT_URI = Uri.parse( “content://com.google.android.MyContentProvider”);
    c. 定义你要返回给客户端的数据列名。如果你正在使用Android数据库,必须为其定义一个叫_id的列,它用来表示每条记录的唯一性。
    d. 创建你的数据存储系统。大多数Content Provider使用Android文件系统或SQLite数据库来保持数据,但是你也可以以任何你想要的方式来存储。
    e. 如果你要存储字节型数据,比如位图文件等,数据列其实是一个表示实际保存文件的URI字符串,通过它来读取对应的文件数据。处理这种数据类型的Content Provider需要实现一个名为_data的字段,_data字段列出了该文件在Android文件系统上的精确路径。这个字段不仅是供客户端使用,而且也可以供ContentResolver使用。客户端可以调用ContentResolver.openOutputStream()方法来处理该URI指向的文件资源;如果是ContentResolver本身的话,由于其持有的权限比客户端要高,所以它能直接访问该数据文件。
    f. 声明public static String型的变量,用于指定要从游标处返回的数据列。
    g. 查询返回一个Cursor类型的对象。所有执行写操作的方法如insert(), update() 以及delete()都将被监听。我们可以通过使用ContentResover().notifyChange()方法来通知监听器关于数据更新的信息。
    h. 在AndroidMenifest.xml中使用<provider>标签来设置Content Provider。

    关于notifyChange():

    我们在ContentProvider的insert,update,delete等改变之后调用getContext().getContentResolver().notifyChange(uri, null);这样就通知那些监测databases变化的observer了,而你的observer可以在一个service里面注册。

    以Downloadmanger为例子: 
    定义ContentObserver,并且在onChange里做你想做的事情:

    /** 
         * Receives notifications when the data in the content provider changes 
         */  
        private class DownloadManagerContentObserver extends ContentObserver {  
      
            public DownloadManagerContentObserver() {  
                super(new Handler());  
            }  
      
            /** 
             * Receives notification when the data in the observed content 
             * provider changes. 
             */  
            public void onChange(final boolean selfChange) {  
                if (Constants.LOGVV) {  
                    Log.v(Constants.TAG, "Service ContentObserver received notification");  
                }  
                updateFromProvider();  
            }  
      
        }
    

    在DownloadService的onCreate中注册: 

     public void onCreate() { 
           super.onCreate(); 
           if (Constants.LOGVV) { 
               Log.v(Constants.TAG, "Service onCreate"); 
           } 
     
           mDownloads = Lists.newArrayList(); 
     
           mObserver = new DownloadManagerContentObserver(); 
           getContentResolver().registerContentObserver(Downloads.CONTENT_URI, 
                   true, mObserver); //注册监听
    .....}

    解开注册:

     public void onDestroy() { 
           getContentResolver().unregisterContentObserver(mObserver); 
           if (Constants.LOGVV) { 
               Log.v(Constants.TAG, "Service onDestroy"); 
           } 
           super.onDestroy(); 
       }

    详细一点可见:http://hi.baidu.com/lck0502/item/77d6ff3f46e52d617c034b60 

    以下还未碰到:

    i. 如果你要处理的数据类型是一种比较新的类型,你就必须先定义一个新的MIME类型,以供ContentProvider.geType(url)来返回。MIME类型有两种形式:一种是为指定的单个记录的,还有一种是为多条记录的。这里给出一种常用的格式:
      vnd.android.cursor.item/vnd.yourcompanyname.contenttype (单个记录的MIME类型)
      比如, 一个请求列车信息的URI如content://com.example.transportationprovider/trains/122 可能就会返回typevnd.android.cursor.item/vnd.example.rail这样一个MIME类型。
      vnd.android.cursor.dir/vnd.yourcompanyname.contenttype (多个记录的MIME类型)
      比如, 一个请求所有列车信息的URI如content://com.example.transportationprovider/trains 可能就会返回vnd.android.cursor.dir/vnd.example.rail这样一个MIME 类型。

    这里似乎可以这么理解:

    1)contentprovider本质上还是类似于数据库SQLite的东西,我们的数据实际上还是使用数据库和文件来存储的,而contentprovider的特殊之处是在于它可以将自己存储的数据提供给外界的APP使用,也就是说,使用contentprovider其实是为了暴露数据,没有这样的需求,使用SQLite即可;

    2)contentprovider除了提供一种将应用本身的数据暴露给别的APP的手段,实际上它还扮演着代理的功能,它的接口将实际的数据操作全部封装了一遍,所有的contentprovider都可以使用相同的接口进行类似的数据操作);

    query()方法,返回值是Cursor实例,用于迭代请求的数据。Cursor是一个接口。android为该接口提供了一些只读的(和 JDBC的ResultSet不一样,后者还提供可写入的可选特性)Cursor实现。比如SQLiteCursor,可迭代SQLite数据库中的数据。可以通过SQLiteDatabase类的query()方法获取到该Cursor实例。还有其他的Cursor实现,比如 MatrixCursor,用于数据不是存储在数据库的情况下

    因为Contentprovider可能被多个ContentResolver对象在不同的进程和线程中调用,因此实现Contentprovider必须考虑线程安全问题。

    作为良好的习惯,在实现编辑数据的代码中,要调用ContentResolver.notifyChange()方法,通知那些监听数据变化的监听器。

    在实现子类的时候,还有一些步骤可以简化Contentprovider客户端的使用:

    定义public static final Uri常量,名称为CONTENT_URI:

    public static final UriCONTENT_URI = Uri.parse("content://com.example.codelab.transportationprovider");

    如果有多个表,它们也是使用相同的CONTENT_URI,只是它们的路径部分不同:

    也就是说红色框部分是一致的。

    定义返回的列名,public static final,列名的值,比如使用SQLite数据库作为存储,对应表的列名。在文档中要写出各个列的数据类型,便于使用者读取。

    四、声明 Content Provider

    创建ContentProvider后,需要在manifest文件中声明,android系统才能知道它,当其他应用需要调用该ContentProvider时才能创建或者调用它。

    语法类似:
    <provider   android:name="com.easymorse.cp.MyContentProvider"
                android:authorities="com.easymorse.cp.mycp"></provider>

    1)android:name要写ContentProvider继承类的全名。

    2)android:authorities要写和CONTENT_URI常量的B部分(见上面图)。

    注意不要把上图C和D部分加到authorities中去。authorities是用来识别ContentProvider的,C和D部分实际上是ContentProvider内部使用的。
    五、一些常用代码
    1)修改数据
    private void updateRecord(int recNo, String name) {
         Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, recNo);
        ContentValues values = new ContentValues();
        values.put(People.NAME, name);
         getContentResolver().update(uri, values, null, null);
     }

    2)插入数据

    private void insertRecords(String name, String phoneNo) {
        ContentValues values = new ContentValues();
        values.put(People.NAME, name);
        Uri uri = getContentResolver().insert(People.CONTENT_URI, values);
        Log.d(”ANDROID”, uri.toString());
        Uri numberUri = Uri.withAppendedPath(uri, People.Phones.CONTENT_DIRECTORY);
        values.clear();
        values.put(Contacts.Phones.TYPE, People.Phones.TYPE_MOBILE);
        values.put(People.NUMBER, phoneNo);
        getContentResolver().insert(numberUri, values);
    }

    3)删除数据

    private void deleteRecords() {
        Uri uri = People.CONTENT_URI;
        getContentResolver().delete(uri, null, null);
    }

    你也可以指定WHERE条件语句来删除特定的记录:

     
    getContentResolver().delete(uri, “NAME=” + “‘XYZ XYZ’”, null);

    这将会删除name为‘XYZ XYZ’的记录。

     下面来一个完整的演示

     

     第一个类:

     public class MyUsers {    

    public static final String AUTHORITY  = “com.wissen.MyContentProvider”;

        // BaseColumn类中已经包含了 _id字段   

    public static final class User implements BaseColumns {        

    public static final Uri CONTENT_URI  = Uri.parse(”content://com.wissen.MyContentProvider”);         // 表数据列        

    public static final String  USER_NAME  = “USER_NAME”;    

    }

    }

    第二个类(继承ContentProvider):

     

    package com.example.filestudy;
    
    import android.content.ContentProvider;
    import android.content.ContentUris;
    import android.content.ContentValues;
    import android.content.Context;
    import android.database.Cursor;
    import android.database.SQLException;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteOpenHelper;
    import android.database.sqlite.SQLiteQueryBuilder;
    import android.net.Uri;
    
    public class MyContentProvider extends ContentProvider {
        private SQLiteDatabase    sqlDB;
        private DatabaseHelper    dbHelper;
        private static final String  DATABASE_NAME = "Users.db";
        private static final int  DATABASE_VERSION= 1;
        private static final String TABLE_NAME= "User";
        private static final String TAG = "MyContentProvider";
    
        private static class DatabaseHelper extends SQLiteOpenHelper {//###管理数据库
            DatabaseHelper(Context context) {
                super(context, DATABASE_NAME, null, DATABASE_VERSION);
            }
    
            @Override
            public void onCreate(SQLiteDatabase db) {
                //创建用于存储数据的表
                    db.execSQL("Create table " + TABLE_NAME + "( _id INTEGER PRIMARY KEY AUTOINCREMENT, USER_NAME TEXT);");
            }
    
            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
                db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
                onCreate(db);
            }
        }
    
        @Override
        public int delete(Uri uri, String s, String[] as) {
            return 0;
        }
    
        @Override
        public String getType(Uri uri) {//这个方法和上面的未碰到的代码有关
            return null;
        }
    
        @Override
        public Uri insert(Uri uri, ContentValues contentvalues) {
            sqlDB = dbHelper.getWritableDatabase();
            long rowId = sqlDB.insert(TABLE_NAME, "", contentvalues);//插入数据
            if (rowId > 0) {
             //Appends the given ID to the end of the path.(也就是说获取插入数据行的Uri)
                Uri rowUri = ContentUris.appendId(MyUsers.User.CONTENT_URI.buildUpon(), rowId).build();
                getContext().getContentResolver().notifyChange(rowUri, null);//检测变化
                return rowUri;
            }
            throw new SQLException("Failed to insert row into " + uri);
        }
    
        @Override
        public boolean onCreate() {//创建数据库
            dbHelper = new DatabaseHelper(getContext());
            return (dbHelper == null) ? false : true;
        }
    
        @Override
        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
            SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
            SQLiteDatabase db = dbHelper.getReadableDatabase();
            qb.setTables(TABLE_NAME);
            Cursor c = qb.query(db, projection, selection, null, null, null, sortOrder);
            c.setNotificationUri(getContext().getContentResolver(), uri);
            return c;
        }
    
        @Override
        public int update(Uri uri, ContentValues contentvalues, String s, String[] as) {
            return 0;
        }
    }

     

    第三个类:测试

     

    package com.example.filestudy;
    
    import android.net.Uri;
    import android.os.Bundle;
    import android.app.Activity;
    import android.content.ContentValues;
    import android.database.Cursor;
    import android.graphics.PointF;
    import android.graphics.Rect;
    import android.view.Menu;
    import android.view.View;
    import android.widget.Button;
    import android.widget.TextView;
    
    public class MainActivity extends Activity {
        Button button;
        TextView view;
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            
            view = (TextView)findViewById(R.id.textview);
            button = (Button)findViewById(R.id.button);
            button.setOnClickListener(new Button.OnClickListener(){
                @Override
                public void onClick(View arg0) {
                    // TODO Auto-generated method stub
                    insertRecord("LQM");
                    displayRecords();
                }
            });
        }
        
        private void insertRecord(String userName) {
            ContentValues values = new ContentValues();
            values.put(MyUsers.User.USER_NAME, userName);
            getContentResolver().insert(MyUsers.User.CONTENT_URI, values);
        }
    
        private void displayRecords() {
            String columns[] = new String[] { MyUsers.User._ID, MyUsers.User.USER_NAME };
            Uri myUri = MyUsers.User.CONTENT_URI;
            Cursor cur = managedQuery(myUri, columns,null, null, null );
            if (cur.moveToFirst()) {
                String id = null;
                String userName = null;
                do {
                    id = cur.getString(cur.getColumnIndex(MyUsers.User._ID));
                    userName = cur.getString(cur.getColumnIndex(MyUsers.User.USER_NAME));
                    view.setText(id + " : " + userName);
               } while (cur.moveToNext());
           }
        }
    }

     

    记好加入声明:

    <provider android:name="MyContentProvider" android:authorities="com.wissen.MyContentProvider" />
    //前面说name要写继承类的全名,这里我的MyContentProvider是和主Activity放置在同一个包里面的,所以直接写类名是可以的

    点击按钮:最后的显示结果:

     

     

     

     

     

  • 相关阅读:
    面向接口编程详解(二)——编程实例
    面向接口编程详解(一)——思想基础
    设计模式之面向接口编程
    EF数据注解
    很多人不知道可以使用这种 key 的方式来对 Vue 组件时行重新渲染
    这是最新的一波Vue实战技巧,不用则已,一用惊人
    Node.js 进阶-你应该知道的 npm 知识都在这
    Vue响应式原理
    eslint规则
    简述vue-cli中chainWebpack的使用方法
  • 原文地址:https://www.cnblogs.com/EggKiller/p/3410465.html
Copyright © 2011-2022 走看看