内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据数据共享的功能,同时这也是Android实现跨程序共享数据的标准方式。尽管之前学习的SharedPreference的数据存储也能够实现读写操作,但是内容提供器可以选择对哪一部分的数据进行共享,因此内容提供器有着更好的保护隐私的作用。
使用系统内容提供器
如果一个应用程序通过内容提供器对其数据提供了外部访问借接口,那么其他的程序就能够通过该接口来对这些数据进行访问。Android系统中自带的电话簿、相册、短信等应用都设置内容提供器,所以我们的程序都可以来利用这些数据。
1.内容URI的组成
由于是其他的程序在访问内容提供器中的数据,所以需要用有一定格式规范的内容URI来代替,我们先来了解一下什么是URI吧。
内容URI给内容提供器中的数据建立了唯一的表示符,它主要是由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,而path则是用于对不同的表做区分的。举例来说假设某个程序中有两张表table 1和table 2,该程序的包为com.example.app,那该程序的两个内容URI的标准写法就是:
content://com.example.app.provider/table 1
content://cmom.example.app.provider/table 2
在得到了内容URI之后,我们还需要将其解析为Uri对象才可以作为参数来进行传入,具体的解析方法是:
Uri uri1 = Uri.parse("content://com.example.app.provider/table 1");
Uri uri2 = Uri.parse("content://com.example.app.provider/table 2");
我们只用调用Uri中的parse()方法即可将其解析为Uri对象,之后我们就可以对该对象进行操作了。
2.ContentResolve类
如果想要访问内容提供器中的内容,我们就需要使用ContentResolve类,使用该类的getContentResolve()方法就会返回一个该类的实例。有了该类的实例我们就可以对其进行CRUD操作,具体的方法仍然是查询query()、添加insert()、更新update()和删除delete()。但是这里的CRUD操作所使用的参数和之前的SQLite有所不同,我们这里不再用单纯的表名指明被操作的表,而是使用内容URI来进行来当参数进行操作了。具体操作的方法和SQLite中的很相似,就用一张图来概括:
3.实践:读取系统联系人
知道了使用方法,下面就来具体的实践一下吧。我们这里是用我们的程序来调用系统的电话簿中的联系人并将他们的姓名和电话号码显示在我们的程序当中。先创建一个空项目,然后为布局加上一个ListView,然后修改主代码:
package com.example.yzbkaka.contasttest;
import android.Manifest;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.provider.ContactsContract;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
List<String> contactList = new ArrayList<String>();
ArrayAdapter<String> adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView contaseView = (ListView)findViewById(R.id.list_view);
adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,contactList);
contaseView.setAdapter(adapter);
if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[] {Manifest.permission.READ_CONTACTS},1);
}
else{
readContast();
}
}
private void readContast(){
Cursor cursor = null;
try{
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
if(cursor != null){
while(cursor.moveToNext()){
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactList.add(displayName+"\n"+number);
}
adapter.notifyDataSetChanged();
}
}catch(Exception e){
e.printStackTrace();
}finally {
if(cursor != null){
cursor.close();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode,String[] permission,int[] grantResults){
switch(requestCode){
case 1:
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
readContast();
}
else{
Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show();
}
}
}
}
这个代码看起来很长,但是其中的onRequestPermissionsResult()方法和中间申请权限的代码都在技能总结篇(2)中见到过。这里主要是看一下readContast()中的操作,我们先是使用getContentResolver()方法返回一个Cursor对象,接着是使用query()方法来进行查询,这个方法中我们传入的ContactsContract.Common DataKinds.Phone.CONTENT_URI类是已经帮我们封装好的Uri对象。接着我们就对cursor对象进行便利,将其中的联系人的姓名和号码读取出来,在使用getColumnIndex()方法时我们传入的参数仍然是系统为我们封装好的常量,我们直接使用即可。最后就是将这些信息传入到List中展示出来。
最后要记住危险权限的声明一定要在AndroidManifest.xml中进行注册:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.yzbkaka.contasttest">
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
创建自己的内容提供器
创建自己的内容提供器可以让我们的程序中的数据共享给其他的程序,我们这里使用的数据库是上一节中使用的Book.dp,我们的创建将会以它为基础来进行。打开那个项目,然后新建一个DatabaseProvider类,并让它继承自ContentProvider类,接着我们在类的开始新建几个变量:
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.example.databasetest.provider";
public static UriMatcher uriMatcher;
MyDatabaseHelper dbHelper;
static{
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY,"book",BOOK_DIR);
uriMatcher.addURI(AUTHORITY,"book/#",BOOK_ITEM);
uriMatcher.addURI(AUTHORITY,"category",CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY,"category/#",CATEGORY_ITEM);
}
我们首先定义了4个常量,BOOK_DIR表示访问Book表中的所有数据,BOOK_ITEM表示访问Book表中的单条数据,CATEGORY_DIR表示访问Category中的全部数据,CATEGORY_ITEM表示访问Category中的单条数据。接着我们在静态代码块中定义了一个UriMatcher实例,利用它的addURI()方法我们可以将期望匹配的几种URI格式添加了进去,这个方法需要传入三个参数,可以分别把authority、path和一个自定义的代码传进去。这样当调用UriMatcher的match()方法时就可以将一个Uri对象传入,而返回值就是一个之前自定义的代码,利用这个代码我们就可以判断出调用方需要的是那一项数据了。
接着我们来分别实现继承过来的方法,首先是onCreate()方法:
@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(),"BookStore.db",null,2);
return true;
}
在这一步中我们是定义了dbHelper,并且让它创建了一个BookStore.db的数据库,然后是返回true表示内容提供器初始化成功。接着来写query()方法:
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Cursor cursor = null;
switch(uriMatcher.match(uri)){
case BOOK_DIR:
cursor = db.query("Book",projection,selection,selectionArgs,null,null,sortOrder);
break;
case BOOK_ITEM:
cursor = db.query("Book",projection,selection,selectionArgs,null,null,sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category",projection,selection,selectionArgs,null,null,sortOrder);
break;
case CATEGORY_ITEM:
cursor = db.query("Category",projection,selection,selectionArgs,null,null,sortOrder);
break;
default:
break;
}
return cursor;
}
在这个方法中需要我们传入5个参数,第一个是Uri对象,第二个是查询的列,第三个和第四个是用于约束查询哪些行,第五个是对结果的排序方式。我们在这个方法里面首先是得到了一个SQLiteDatabase对象,并定义了一个Cursor,之后我们再进行匹配,当匹配成功时,我们就使用SQLiteDatabase中的query()方法来进行查询,该方法会返回一个Cursor对象。在最后我们会返回一个Cursor对象。
接着来看insert()方法:
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)){
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book",null,contentValues);
uriReturn = Uri.parse("content://"+AUTHORITY+"/book/"+newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Category",null,contentValues);
uriReturn = Uri.parse("context://"+AUTHORITY+"/category/"+newCategoryId);
break;
default:
break;
}
return uriReturn;
}
在这个方法中也是先获取SQLiteDatabase对象,然后再来根据uri进行匹配。但是要注意的是这个方法要求返回一个Uri的对象,因此我们需要使用parse()方法来进行转换,不过这个内容的URI是以新增数据的id结尾的。
接下来就是udata()方法:
@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updateDows = 0;
switch (uriMatcher.match(uri)){
case BOOK_DIR:
updateDows = db.update("Book",contentValues,selection,selectionArgs);
break;
case BOOK_ITEM:
String newBookId = uri.getPathSegments().get(1);
updateDows = db.update("Book",contentValues,"id=?",new String[] {newBookId});
break;
case CATEGORY_DIR:
updateDows = db.update("Book",contentValues,selection,selectionArgs);
break;
case CATEGORY_ITEM:
String newCategoryId = uri.getPathSegments().get(1);
updateDows = db.update("Book",contentValues,"id=?",new String[]{newCategoryId});
break;
default:
break;
}
return updateDows;
}
updata()方法也是和前面几个相似,先是获取SQLiteDatabase对象,然后再来根据uri进行匹配。这个方法需要返回一个整数代表的事更新的行数,所以在匹配的时候我们使用了一个getPathSegments()的方法,该方法可以把URI中的的权限之后的部分以“/”分割,并把分割后的结果放在一个字符串列表中,所以这个列表第0个位置存放的就是路径,第1个位置存放的就是id了。
然后是delete()方法了:
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deleteRows = 0;
switch(uriMatcher.match(uri)){
case BOOK_DIR:
deleteRows = db.delete("Book",selection,selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deleteRows = db.delete("Book","id = ?",new String[] {bookId});
break;
case CATEGORY_DIR:
deleteRows = db.delete("Category",selection,selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deleteRows = db.delete("Category","id = ?",new String[]{categoryId});
break;
default:
break;
}
return deleteRows;
}
这个方法也很简单,就不多说了。
最后就是getType()方法了:
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)){
case BOOK_DIR:
return "vnd.android.cursor.dir/vand.com.example.databasetest.provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.dir/vand.com.example.databasetest.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vand.com.example.databasetest.provider.CATEGORY";
case CATEGORY_ITEM:
return "vnd.android.cursor.dir/vand.com.example.databasetest.provider.CATEGORY";
}
return null;
}
这个方法是用于获取Uri对象所对应的MIME类型,一个内容Uri所对应的MIME字符串主要是由三部分构成:Android对这三个部分做了以下格式规定:必须以vnd开头;如果内容URI以路径结尾,则后接android.cursor.dir/,如果内容URI以id结尾,则后接android.cursor.item/;最后接上vnd.< authority>.< path>。所以我们把这四个添加进去。
最后内容提供器一定要在AndroidManifst.xml中进行注册:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.yzbkaka.databasetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:authorities="com.example.databasttest.provider"
android:name=".DatabaseProvider"
android:exported="true"
android:enabled="true">
</provider>
</application>
</manifest>
我们在<application>标签中添加了<provider>,其中authorities就是指它的authority,name指定了DatabaseProvider的类名,exported表示是否运行外部程序访问我们的内容提供器,enable表示是否启用该内容提供器。
最后我们运行程序,然后退出程序,再写一个调用该程序的测试程序,先在布局文件里面添加4个按钮分别表示CRUE,然后修改主代码:
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 添加数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
ContentValues values = new ContentValues();
values.put("name", "A Clash of Kings");
values.put("author", "George Martin");
values.put("pages", 1040);
values.put("price", 55.55);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 查询数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor. getColumnIndex("name"));
String author = cursor.getString(cursor. getColumnIndex("author"));
int pages = cursor.getInt(cursor.getColumnIndex ("pages"));
double price = cursor.getDouble(cursor. getColumnIndex("price"));
Log.d("MainActivity", "book name is " + name);
Log.d("MainActivity", "book author is " + author);
Log.d("MainActivity", "book pages is " + pages);
Log.d("MainActivity", "book price is " + price);
}
cursor.close();
}
}
});
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 更新数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
ContentValues values = new ContentValues();
values.put("name", "A Storm of Swords");
values.put("pages", 1216);
values.put("price", 24.05);
getContentResolver().update(uri, values, null, null);
}
});
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 删除数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}
最后运行程序,调用数据成功!!!