Content Provider

Content Provider是Android四大组件之一,用于管理应用程序访问结构化的数据。Content Provider可以压缩数据,并保护访问数据的安全。Content Provdider是应用程序跨进程数据的标准接口。


使用Content Provider访问数据,你应当在自己的应用程序的Context环境中使用ContentResovler对象,并将该对象作为client对象,与content provider通信(实际上是与content provider的子类进行通信):content provider 接受cilent端发送的请求,并根据请求执行操作,最后将结果返回client端。


如果你不打算把自己应用程序的数据分享给其他应用,那么无需创建自己的content provider。但是,如果需要在自己的程序中提供定制的搜索规范,或是自己的应用向其他应用程序拷贝或粘贴大型而复杂的数据文件,那么就需要创建content provider。


Content Provider基础

provider是Android应用程序的一部分,使用provider提供的数据可以用于UI展示,然而,provider最常用的情形是将本应用的数据提供给其他应用程序使用,使用provider的client(ContentResovler)来访问provider,也就是说,provider一般与resolver配对使用,provider提供了访问数据的标准接口,可以方便resolver进行安全的跨进程通信


请注意:provider提供的表不一定必须提供组件,即便提供组件,名字也不一定必须是_ID,但是,若你需要将provider提供的表绑定到ListView上的话,那么该表必须提供主键,且名字必须是_ID.


1、访问provider

在应用程序中使用ContentResolver对象,可以访问其他应用程序的provider,在该类提供了与ContentProvider类中命名相同的方法,包括增删改查等。

ContentResolver所在进程与ContentProvider是不同的进程,所以这无形中实现了跨进程通信,而ContentProvider实际上还充当了抽象层,该层介于数据库和外部提供数据接口之间。


注意:为了使Content Resolver能访问Content Provider的数据,你需要在清单文件中加入访问权限。

举例来说,若需要获得上表中word和locale列的数据,需要在你的应用程序中调用ContentResolver.query方法,接着该方法又会调用ContentProvider.query()方法,下面演示了访问过程:

mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  
    mProjection,                        
    mSelectionClause                    
    mSelectionArgs,                    
    mSortOrder);                        

各个参数的含义:

uri:表的uri地址

projection:要查询的列

selection:制定要选择的行

selectionArgs:补充前面的字符

sortOrder:排序


2、内容URI:

内容URI指向了provider提供的一组数据,内容URI由两部分组成,其中authority部分指定了需要访问的provider名,path部分指定了该provider的某张表。

在上述代码中,Content_URI指向了单词表,ContentResolver从Content_URI中解析出authority,并在系统中查询能与之相配的provider,找到该provider后,该provider利用URI的path部分寻找匹配的表,所以,综上所述,word表应该是如下uri:

content://user_dictionary/words

其中user_dictionary是authority,words是path,content://则是固定的,表示这是一个内容uri。

你也可以在URI后缀一个_ID来查询表中的某一行数据

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
这表示希望查询单词表中_ID为4的那一行数据,当你需要删除或者更新某一行数据时,应当使用这种加入_ID的URI


请注意:Uri类与Uri.Builder类提供了便捷的方法,把String包装为格式规范的URI对象,而ContentUri类同样提供了便捷的方法,在URI后追加ID,上述代码的withAppendedId()就是为Uri追加ID的方法。


3、从Provider中查询数据
为了清晰起见,本节调用的ContentResolver.query()都在UI线程中进行,但在实际开发中,这应该是一个异步操作--方法应该在子线程中进行,方法之一是使用CursorLoader。

从provider中查询数据,应按照如下操作进行

(1)声明读取数据的访问权限

(2)发送查询请求

//要查询的列
    String[] mProjection = {
            UserDictionary.Words._ID,
            UserDictionary.Words.WORD,
            UserDictionary.Words.LOCALE
    };
    //要查询的项
    String mSelectionClause = null;
    //查询项的字符
    String[] mSelectionArgs = {""};

下面的代码演示了从UI界面中获取用户输入的单词,并在字典表中查询表中是否包含该单词,若包含,用Cursor对象返回单词在表中所在行的信息

public class MainActivity extends AppCompatActivity {
    Cursor mCursor;
    //要查询的列
    String[] mProjection = {
            UserDictionary.Words._ID,
            UserDictionary.Words.WORD,
            UserDictionary.Words.LOCALE
    };
    //要查询的项
    String mSelectionClause = null;
    //查询项的字符
    String[] mSelectionArgs = {""};
    TextView mSearchWord;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //从UI中获取String
       String mSearchString= mSearchWord.getText().toString();

        //记得用代码来检查无效的或者恶意的输入

        //如果该String不为空,获取所有数据
        if (TextUtils.isEmpty(mSearchString)) {
            //把mSelectionClause设置为null返回所有数据
            String mSelectionClause = null;
            mSelectionArgs[0] = "";
        }
        else {
            //根据用户输入的词来构建一个选择项
            mSelectionClause=UserDictionary.Words.WORD+"=?";

            mSelectionArgs[0] = mSearchString;
        }
        //进行查询并返回Cursor
        mCursor=getContentResolver().query(
                UserDictionary.Words.CONTENT_URI,
                mProjection,
                mSelectionClause,
                mSelectionArgs,
                null);
        //如果有错误发生一些provider会返回null,
        if (mCursor == null) {
            //在这里用代码处理错误,并且一定不能使用cursor
        }
        //如果cursor是空的,说明没有匹配的结果
        else if (mCursor.getCount()<1){
            //在这里插入代码来通知用户搜索失败,这不一定是个错误,可以提示用户插入一个新的搜索词
        }
        else {
            //在这里返回查询结果
        }
    }
}

(3)防止恶意输入

考虑到下面这种情况

Constructs a selection clause by concatenating the user's input to the column name
String mSelectionClause =  "var = " + mUserInput;

这种输入方式会给数据库带来隐患,用户可以输入诸如nothing,drop table *等内容,当程序读到用户的输入时,mSelectionClause变量中的内容将会是var = nothing; DROP TABLE *;`,这将导致这张表被删除。


为避免上述情况,应使用?作为占位符,以分离SQL语句和条件筛选参数,因为这种输入方式,系统将不再把用户的输入作为SQL语句的一部分,用户无法再恶意破坏数据库内容。


(4)显示查询结果

client端的ContentResovler.query()方法返回一个Cursor对象,他指向了按筛选条件查询的结果,调用Cursor中的方法,你可以遍历结果中的每一行,并通过表的字段名得到该行中的每一列信息,当provider的数据发生改变时,Cursor的数据自动改变,或者触发observer对象中的方法。

若没有行能匹配查询条件,Cursor.getcount()将返回0。

若查询过程发生了错误,Cursor对象将返回null,或者抛出一个异常.。

由于Cursor返回的是一个列表,可以通过simpleCursorAdapter将列表内容显示在ListView上,如下所示:

   //列名
    String[] mWordListColumns={
            UserDictionary.Words.WORD,
            UserDictionary.Words.LOCALE
    };
    //定义视图id
    int mWordListItems={R.id.dictWord, R.id.locale};
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //simpleCursorAdapter
        CursorAdapter mCursorAdapter=new SimpleCursorAdapter(
                getApplicationContext(), //context对象
                R.layout.wordlistrow,    //一行的xml布局文件
                mCursor,                 //返回的cursor
                mWordListColumns,     //列名
                mWordListItems,       //每行的视图id
                0                       //flag(一般不需要)
        );
        mWordList.setAdapter(mCursorAdapter);

请注意:为了使ListView能显示Cursor中的数据,Cursor中必须有一列_ID字段,即使ListView不显示该列,Cursor也必须包含该列,所以在建表时,应设置_ID字段。


(5)从Cursor中获取数据

下面的代码演示了如何从Cursor中获取Word字段中的数据

 public void getData(){
        //定义word列是第几列
        int index=mCursor.getColumnIndex(UserDictionary.Words.WORD);

        /**
         * 只有cursor是有效的才会执行,一些provider在发生错误时会返回null,而另外一些可能抛出异常
         */
        if (mCursor!=null){
            /**
             * 光标移动到下一行,在移动到底一行之前,行指针为-1,如果你想在这个位置获取数据的话会得到一个异常
             */
        while (mCursor.moveToNext()){
            //获取值
            String word = mCursor.getString(index);

            //插入代码来检索词
        }

        }
        else {
            //插入代码来处理错误
        }
    }

(6)Content Provider权限

在应用程序设置中,需要为provider设置权限,其他应用程序只有获得了权限,才能访问provider。为provider设置权限保证了provider知道访问他的应用程序需要获得什么数据,这保证了provider所管理的数据安全。


如果provider未制定任何权限,其他应用将无法访问,不过,无论provide是否设置了权限,与provider处于同一应用的组件有完全的读写provider权限。


正如上面提到的,用户字典需要android.permission.READ_USER_DICTONARY权限才能被其他应用所访问,当然,改权限只是具有读取provider的权限,如需要对provider所管理的数据进行增、删、改的操作还需要声明写入权限。


声明这些权限需要在清单文件的中进行,这些声明的权限将在应用安装时在应用列表以列表的形式呈现给用户,若用户允许则安装应用,不允许则安装失败


(7)增删改数据

与查询类似,使用resolver同样可以对provider进行操作

1)插入数据

调用ContentResolver.insert()方法可以向provider中插入数据,该方法向provider中的某个表插入一行数据,并将该行数据的内容URI返回,下面为演示:

 public void add(){
        //定义一个新的uri对象来接收插入的结果
        Uri mUri;

        //定义一个对象来包含要插入的新值
        ContentValues mNewValues=new ContentValues();

        /*
        把值设置给每一列并且插入word,这两个值分别是列名和值
         */
        mNewValues.put(UserDictionary.Words.APP_ID,"example.user");
        mNewValues.put(UserDictionary.Words.LOCALE,"en_US");
        mNewValues.put(UserDictionary.Words.WORD,"insert");
        mNewValues.put(UserDictionary.Words.FREQUENCY,"100");

        mUri=getContentResolver().insert(
                UserDictionary.Words.CONTENT_URI,//用户字典的uri
                mNewValues);
    }

如果你打算添加一行却不添加任何数据的话,可以调用ContentValues.putNull()传入空值。

在增加一行时,无需指定_ID字段的值,因为在一般情况下,将_ID设为主键和自增长,该字段将被自动赋予值。


返回的uri格式如下:

content://user_dictionary/words/
上面的id_value表示新加入的id号,如需从返回的Uri对象中获取该ID值,可以调用ContentUri.parseId()。


2)更新数据

在client端调用ContentResovler.update()方法可以更新一条数据,你同样可以使用ContentValue对象修改数据,如需将某一字段下的数据清空,直接传入null即可。

下面演示了将provider管理的用户字典中的local字段含有“en”的数据条目清空,返回值是修改的行数:

   public void update(){
        ContentValues mUpdateValues=new ContentValues();

        //定义想要修改哪些行
        String mSelectionClause=UserDictionary.Words.LOCALE+"LIKE ?";
        String[] mSelectionArgs={"en_%"};

        //定义一个变量来接收修改的行数
        int mRowsUpdated=0;

        mUpdateValues.putNull(UserDictionary.Words.LOCALE);
        mRowsUpdated=getContentResolver().update(
                UserDictionary.Words.CONTENT_URI,
                mUpdateValues,
                mSelectionClause,
                mSelectionArgs
        );
    }

3)删除数据

调用ContentResolver.delete删除表中指定的数据,下面演示了删除表中appid字段中包含user的条目:

 public void delete(){
        //定义想要修改哪些行
        String mSelectionClause=UserDictionary.Words.APP_ID;
        String[] mSelectionArgs={"user"};

        //定义一个变量来接收删除的行数
        int mRowsDeleted=0;

        mRowsDeleted=getContentResolver().delete(
                UserDictionary.Words.CONTENT_URI,
                mSelectionClause,
                mSelectionArgs
        );
    }


(8)Provider管理的数据类型

除了用户字典中的text类型,provider还支持下列类型操作:

integer

long

float

double

provider还经常使用BLOB(Binary Large OBject),它是最大容量为64KB的字节数组,他可以查询Cursor类中若干get方方法来查看provider支持的类型


provider为每一个访问或返回cilent端的内容URI对应了一个MIME数据类型,通过该MIME数据类型可以判断你的应用程序是否支持该类型。当provider提供了复杂的数据类型时,你可能需要MIME数据类型的帮忙。如,通讯录应用程序的contentprovider包含一张ContractsContract.data的表,在表中每一行,就是通过MIME类型标记联系人的数据信息,如需获得content URI中的MIME类型,可以调用ContentResolver.getType()。


4、访问Provider的其他方式

除了上面介绍的访问Content Provider的方式外,还有三种方式

1)批量访问:你可以使用ContentProviderOperation类对provider进行批量访问,并在cilent调用ContentResovler.applyBatch()进行批量访问。

当需要对表中的多行数据、或者需要对多表进行操作时,这就需要批量操作,除此之外,实现数据库的事务操作,也需要批量操作。为了实现批量模式,需要一组ContentProviderOperation对象,并在cilent端调用ContentResolver.applyBatch(),向该方法中传入provider的authority,而不是content URI,因为这可以对不同表进行操作,ContentResovler.applyBatch()方法将返回一组结果

2)异步查询:你应当在子线程执行查询操作,可以使用CursorLoader类来实现。

3)通过Intent访问数据,虽然不能直接向provider传递数据,但是可以向provider所在的应用传递,这种方式提供了修改provider数据的最齐全方式。

向应用程序发送intent,即便没有访问该应用程序中的provider权限,你依然可以访问provider的数据,甚至通过返回的intent还能获得URI权限,你可以设置flag获取intent中的权限:

读取权限:FLAG_GRANT_READ_URI_PERMISSION

写入权限:FLAG_GRANT_WRITE_URI_PERMISSION


provider在清单文件中通过android:grantUriPermission属性、以及provider的子标签定义了URI权限


下面的例子演示了如何从通讯录应用中的provider管理表中获取联系人信息(无需声明READ_CONTRACT权限)

1)调用StartActivityForResult()方法,传入intent,intent应包含ACTION_PICK的action,和contract的MIME类型CONTENT_ITEM_TYPE

2)这会启动通讯录应用的selectionActivity,该activity会切至前台

3)在selection activity中,用户会选择某个联系人并修改其信息,此时,setResult(resultCode,intent)方法将被回调,并将intent回传至你的应用中,intent包含了用户选择的联系人content URI,并在extra中包含了FLAG_GRANT_READ_URI_PERMISSON的flag,该flag提供了你的应用程序访问联系人中指定数据的权限,最后,selectionactivity调用finish()destory

4)你的activity切回前台并回调onActivityResult()方法获得回传的intent,从intent中获取的content URI可以直接访问联系人应用中的provider数据而无需在清单文件中声明权限


5、MIME类型引用

MIME类型使用

provider可以返回标准或定制的MIME类型字符串

MIME类型的标准形式:

type/subtype
比如,常用的MIME类型text/html,表示其指向的数据包含text类型和html类型,若provider返回的uri中包含的MIME类型为text/html,那么该返回结果将包含html标签。

定制的MIME类型,也称作vendor-specific类型,他具有更复杂的结构

其中type类型中:

vnd.android.cursor.dir表示uri指向的数据包含多行,而vnd.android.cursor.item表示包含一行。

subtype时provider定制的(provider-specific),系统自带的provider通常包含一个简单的subtype,如通讯录应用中增加一行电话号码的记录,那么指向它的URI中的MIME类型将为:

vnd.android.cursor.item/phone_v2


你可能感兴趣的:(Content Provider)