0%

跨程序共享数据--Android之内容提供器(ContentProvider)学习

一、访问其他程序中的数据

内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。

二、ContentProvider的基本用法

想要访问一个应用程序中共享的数据,就要借助ContentResolver这个类,可通过Context的getContentResolver()得到这个类的实例。ContentResolver类提供了一系列的方法用于对于进行CRUD操作,其中insert()用于添加数据,update()用于更新数据,delete()方法用于更新数据,query()方法用于查询数据。SQLiteDatabase也是用这几个方法来进行CRUD操作,只不过他们在方法参数上有些区别。

不同于SQLiteDatabase,ContentResolver类的CRUD是不接收表名参数的,而是使用Uri参数代替,它被称为内筒URI,它给内容提供器中的数据建立了唯一的标识符,由两部分组成:authority和path,authority是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是com.example.app,那么该程序的authority就可命名为com.example.app.provider;path则是一般用于对同一应用的不同表做区分的,通常会添加到authority后面,如有两张表table1、table2,path就可命名为/table1和/table2,然后将authority与path进行组合,内容URI就成为了com.example.app.provider/table1和com.example.app.provider/table2。我们会在这个字符串的头部加上协议声明:

1
2
content://com.example.app.provider/table1
content://com.example.app.provider/table2

内容URI可以非常清楚地表达我们想要访问哪个程序中哪张表的数据。也正因如此,ContentResolver中的CRUD方法才都接收Uri对象作为参数,因为如果使用表名的话,系统将无法得知我们期望访问的是哪个应用程序里的表。

得到内容URI字符串之后,我们还要将它们解析成Uri对象才可以作为参数传入,解析的方法如下:

1
Uri uri = Uri.parse("content://com.example.app.provider/table1");

只需要调用Uri.parse()方法,就可以将内容Uri字符串解析成Uri对象了。解析之后我们就可以使用这鞥Uri对象来查询table1中的数据了,代码如下:

1
2
3
4
5
6
7
Cursor cursor = getContentResolver().query(
uri,
projection,
selection,
selectionArgs,
sortOrder
);

查询完成后返回的是一个Cursor对象,这时候我们可以将Cursor对象从Cursor对象中逐个读取出来,读取的思路仍然是通过移动游标的位置来遍历Cursor的所有行,然后再取出每一行中相应列的数据,代码:

1
2
3
4
5
6
if (cursor != null) {
while (cursor.moveToBext) {
String column1 = cursor.getString(cursor.getColumnIndex("column1");
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
}

向table1中添加一条数据

1
2
3
4
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column1",1);
getContentResolver().insert(uri,values);

可以看到,是将待添加的数据组黄到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入即可。
如果想更新这条添加的数据,把column1的值清空,可以借助ContentResolver的update方法实现,代码如下:

1
2
3
ContentValues values = new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1 = ? and column2 = ?",new String[] {"text","1"});

1,读取联系人

项目源代码地址:读取联系人(ContactsTest)
这是一个读取手机联系人信息的项目,我们希望读取联系人的信息并能够在listview中显示出来,修改activity_main.xml,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ListView
android:id="@+id/contacts_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>


</LinearLayout>

在MainActivity的代码:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class MainActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView contactsView = findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,contactsList);
contactsView.setAdapter(adapter);
if (ContextCompat.checkSelfPermission(this,Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,new String[] {Manifest.
permission.READ_CONTACTS},1);
}else {
readContacts();
}
}

private void readContacts () {
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));
contactsList.add(displayName + "\n" + number);
}
adapter.notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.
PERMISSION_GRANTED) {
readContacts();
} else {
Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}

在onCreate方法中,首先获取了ListView的实例,并给它设置好了适配器,然后开始调用运行时权限的处理逻辑,因为READ_CONTACTS属于危险权限,所以要先获取权限。获得授权之后调用readContacts()方法来读取联系人信息。运行程序,首先会弹出申请访问联系人权限的对话框,我们点击”始终允许”,当然也可以点击拒绝。


点击允许后,程序会显示联系人信息,如下图所示:

readContacts方法解析:

  • 这里使用了ContentResolver的query()方法来查询联系人的数据,不过传入的Uri参数为什么没有调用Uri.parse()方法去解析一个内容URI字符串呢?因为ContactsContract.CommomDataKinds.PHhone()类已经帮我们做好了封装,提供了一个CONTENT_URI常量,而这个常量就是使用Uri.parse()解析出来的结果。接着对Cursor对象进行遍历,将联系人姓名和手机号这些数据逐个取出,联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds.DISPLAY_NAME,联系人手机号对应的常量是ContactsContract.CommonDataKinds.PHONE_NUMBER。两个数据都取出之后,将它们进行拼接,并且在中间加上换行符,然后将拼接后的数据添加到ListView的数据源里,并通知刷新一下ListView,最后要把cursor对象关闭。
  • 还有读取联系人的权限”android.permission.READ_CONTACTS”要在清单文件里声明。

总结

  • 想要访问内容提供器中共享的数据,必须借助ContentResolver类
  • ContentResolver的增删改查方法都不接收表明参数,而是使用Uri作为参数,我们称它为内容URI
  • 得到内容URI字符串后,我们要将它解析成Uri对象才可以作为参数传入(Uri.parse()方法)
  • 通过Uri对象查询数据,返回的是一个Cursor对象,这个时候我们就可以将数据从Cursor对象中逐个读取出来

三、创建自己的内容提供器

前面我们学习了如何在自己程序中访问其他应用程序的数据。总体来说思路还算非常简单,只需要获取到该应用程序的内容URI,然后借助ContentResolver进行CRUD操作就可以了。问题来了?这些提供外部访问接口的应用程序如何实现这种功能的呢?他们又是怎样保证数据的安全性呢,从而能使得隐私数据不会泄露出去?接下来一一讲解。

1,创建内容提供器的步骤

前面提到过,如果想跨程序共享数据,官方推荐的是用内容提供器,可以新建一个类去继承ContentProvider的方式来创建一个自己的内容提供器,ContentProvider类有6个抽象方法,我们在使用子类继承它的时候,需要将这6个抽象方法全部复写,新建MyProvider继承ContentProvider,代码如下:

1
2


2,实现跨程序数据共享

我们在上一章的DatabaseTest项目的基础上继续开发,通过内容提供器来给它加入外部访问接口。打开DatabaseTest项目,首先将MyDatabaseHelper中使用Toast弹出创建数据库成功的提示去除掉,因为跨程序访问时不能直接使用Toast。然后创建一个内容提供器,右键点击报名–>New–>Other–>ContentProvider,将内容提供器命名为DatabaseProvider,authority指定为com.example.databasetest.provider。选中Exported和Enabled属性。
修改DatabaseProvider中的代码,如下所示:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
public class DatabaseProvider extends 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";

private static UriMatcher uriMatcher;

private 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);
}

@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
return true;
}

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
// 查询数据
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id = ?", new String[] { bookId }, null, null, sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
cursor = db.query("Category", projection, "id = ?", new String[] { categoryId }, null, null, sortOrder);
break;
default:
break;
}
return cursor;
}

@Override
public Uri insert(Uri uri, ContentValues values) {
// 添加数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Category", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
break;
default:
break;
}
return uriReturn;
}

@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
// 更新数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id = ?", new String[] { bookId });
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", values, "id = ?", new String[] { categoryId });
break;
default:
break;
}
return updatedRows;
}

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 删除数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deletedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deletedRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
break;
case CATEGORY_DIR:
deletedRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deletedRows = db.delete("Category", "id = ?", new String[] { categoryId });
break;
default:
break;
}
return deletedRows;
}

@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.category";
}
return null;
}

}

另外注意,内容提供器一定要在清单文件注册,Android studio已经帮我们自动注册了(因为我们是用Android Studio的快捷功能创建内容提供器的),打开AndroidManifest.xml文件,发现注册已经自动完成了。也就是在标签多了一个标签。
现在DatabaseTest已经具有跨程序共享数据的功能了,我们新建一个ProviderTest项目,然后通过这个程序去访问DatabaseTest中的数据。新建ProviderTest项目,修改activity_main.xml,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<Button
android:id="@+id/add_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add To Data"/>

<Button
android:id="@+id/query_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Query From Book"/>

<Button
android:id="@+id/update_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Update Book"/>
<Button
android:id="@+id/delete_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Delete From Book"/>

设置为垂直布局,设置四个按钮,分别用于添加、查询、修改和删除数据。然后修改MainActivity中的代码:

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
private String newId;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Button addData = 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);
}
});

}

我们在四个按钮的点击事件里面处理了CRUD的逻辑

  • 添加数据的时候,调用了Uri.parse()方法将一个内容URI解析成Uri对象,然后把要添加的数据都存放到ContentValues对象中,接着调用ContentResolver的insert()方法执行添加操作就可以了。注意insert()方法会返回一个Uri对象,这个对象中包含了新增数据的id,我们通过getPathSegments()方法将这个id取出。
  • 查询数据的时候,调用Uri.parse()方法将一个内容URI解析成Uri对象,然后调用ContentResolver的query()方法去查询数据,查询的结果还是在Cursor对象中,之后对Cursor进行遍历,从中取出查询结果,并一一打印出来。
  • 更新数据,也是将内容URI解析成Uri对象,然后把想要更新的数据存放到ContentValues对象中,再调用ContentResolver的update()方法执行更新操作就可以了。这里为了不想让Book表中的其他行受到影响,在调用Uri.parse()方法的时候,给内容URI的尾部增加了一个id,而这个id正是添加数据是所返回的。这就表明我们只希望更新刚刚添加的那条数据,Book表中的其他行不会受到影响。
  • 删除数据,也是同样的方法解析一个以id结尾的内容URI,然后调用ContentResolver的delete方法执行删除操作就可以了,由于我们在内容URI里指定了一个id,因此只会删除以拥有相应id的那行数据,Book表中的其他数据不会受影响。

点击ADD TO BOOK按钮,此时数据就应该添加到了DatabaseTest程序的数据库中了,再通过点击Query From Book按钮检查,日志如图:


然后再点击Update Book按钮完成更新数据的操作,再点击一下Query From Book按钮进行检查,日志如图:


最后点击Delete From Book按钮,此时再点击Query From Book按钮查询数据,我们会发现查询不到数据了。由此可以看出,我们的跨进程访问数据功能实现了,现在不仅仅ProviderTest程序吗,任意一个程序都可以访问DatabaseTest程序中的数据,而且不需要担心隐私数据泄露的问题。