内容提供器探究

内容提供器简介

内容提供器主要用于在不同的应用程序之间实现数据共享的功能,并且保证被访问数据的安全性。

运行时权限

Android6.0系统中引入了运行时权限。

Android权限机制详解

运行时权限即可以在软件的使用过程中再对某一项权限申请授权。

Android的权限归为两类:
普通权限:不会直接威胁到用户的安全和隐私的权限。对于这部分权限申请,系统会自动帮我们进行授权。

危险权限:可能会触及用户隐私,或对设备安全性造成影响的权限。必须由用户手动点击授权才可以。

当用户同意授权某一危险权限时,则其权限组中其他权限也会同时被授权。

危险权限表:

可以访问http://developer.android.com/reference/android/Manifest.premission.html查看Android系统中的完整权限列表。

在程序运行时申请权限

新建一个RuntimePermissionTest项目。

添加按钮,并修改代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button make_call = (Button) findViewById(R.id.make_call);
make_call.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try{
Intent intent =new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch(SecurityException e) {
e.printStackTrace();
}
}
});
}

并添加权限:

1
<uses-permission android:name="android.permission.CALL_PHONE" />

编译运行后,在Android4.4版本中点击按钮后可以发现自动打开了拨号界面。但是

在Android6.0版本中,点击按钮没有任何反应,查看logcat中断日志,可以看到:java.lang.SecurityException: Permission Denial: starting Intent的字样。

这是因为6.0以上的系统在使用危险权限时都必须进行运行时权限处理。

修改代码如下:

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
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button make_call = (Button) findViewById(R.id.make_call);
make_call.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this, new String[]{ Manifest.permission.CALL_PHONE}, 1);
}else{
call();
}
}
});
}
private void call(){
try{
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}catch(SecurityException e) {
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults){
switch(requestCode){
case 1:
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
call();
} else {
Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:

}
}
}

编译运行后点击按钮,在6.0版本以上就会弹出授权窗口(第一授权后,继续点击则默认已授权,需卸载后才会恢复权限设置,或者在权限设置关闭权限),同意后就会自动跳转到拨号界面了。其中ActivityCompat.requestPermissions()的第三个参数为请求码,用于下面重写函数onRequestPermissionsResult()的switch。

访问其他程序中的数据

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

如果一个应用程序通过内容提供器对其数据提供了外部访问接口,那么任何其他的应用程序就都可以对这部分数据进行访问。

ContentResolver的基本用法

ContentResolver提供了一系列方法用于对数据进行CRUD操作,可以用Context中的getContentResolver()方法获取该类的实例

ContentResolver中的增删改查方法都是不接收表名参数的,而是用一个Uri参数代替,这个参数被称为内容URI。

内容URI给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority用于对不同的应用程序做区分,path则是用于对同一应用程序中不同的表做区分,通常会添加到authority后面。内容URI最标注的格式写法如下:

1
content://com.example.app.provide/table1

读取系统联系人

新建一个ContactsTest项目,修改布局文件为如下:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

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

然后修改代码如下:

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
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 = (ListView) findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<String>(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, String[] permissions, 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:
}
}
}

并且添加权限声明:

1
<uses-permission android:name="android.permission.READ_CONTACTS" />

然后编译运行并授权就可以了。

创建自己的内容提供器

创建内容提供器的步骤

打开DataTest项目,去掉MyDatabaseHelper中的Toast提示,因为跨程序访问时我们不能直接使用Toast。

新建Content Provider,命名为DatabaseProvider,authority指定为com.example.databasetest.provider,修改代码如下:

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
public class DatabaseProvider extends ContentProvider {

public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIT = 2;
public static final int CATEGROY_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, "categpory", CATEGORY_DIT);
uriMatcher.addURI(AUTHORITY, "category/#", CATEGROY_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_DIT:
cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
break;
case CATEGROY_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_DIT:
case CATEGROY_ITEM:
long newBookId = db.insert("Category", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newBookId);
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_DIT:
updatedRows = db.update("Category", values, selection, selectionArgs);
break;
case CATEGROY_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_DIT:
deletedRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGROY_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_DIT:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category";
case CATEGROY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category";
}
return null;
}
}

现在DatabaseTest这个项目就已经拥有了跨程序共享数据的功能了。

接着新建一个ProviderTest项目来访问数据:
修改布局:

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
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/add"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="ADD"
/>
<Button
android:id="@+id/query"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="QUERY"
/>
<Button
android:id="@+id/update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="UPDATE"
/>
<Button
android:id="@+id/delete"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="DELETE"
/>
</LinearLayout>

然后修改代码:

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
public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button add = (Button) findViewById(R.id.add);
add.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", 22.85);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});


Button update = (Button) findViewById(R.id.update);
update.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("author", "George Raymond Richard Martin");
values.put("pages", 1216);
values.put("price", 24.05);
getContentResolver().update(uri, values, null, null);
}
});

Button query = (Button) findViewById(R.id.query);
query.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.moveToFirst()) {
do {
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);
} while (cursor.moveToNext());
}
cursor.close();
}
});

Button delete = (Button) findViewById(R.id.delete);
delete.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);
}
});
}
}

这样即可完成跨程序的内容提供器的创建与操作了。

总结

Android权限分为2类:普通权限和危险权限。在Android6.0版及以上本引入了运行时权限,用户需要手动授予危险权限才能使用,并且一旦授权其对应权限组下其他权限也会被授权。

内容提供器提供了跨程序数据共享的功能,与数据库存储方式类似,可以使用增删改查四个功能,但其参数略有不同。

前文实践中在创建的内容提供器内部引入SQLite数据库的操作,并重写增删改查功能,以实现对外部程序共享数据的功能。