数据持久化技术,包括文件存储、SharedPreferences存储以及数据库存储,都只能在当前应用程序中访问。跨程序数据共享需要用到另一种技术——内容提供器。
7.1 内容提供器简介 内容提供器(Content Provider)主要用于不同的应用程序之间实现数据共享的功能,同时保证被访数据的安全性,是实现跨程序共享数据的标准方式。
7.2 运行时权限 Android权限机制作用比较有限,容易出现“店大欺客”现象。因此在Android 6.0引入运行时权限。
7.2.1 Android权限机制详解 为了访问系统的网络状态以及监听开机广播,于是在
AndroidManifest.xml
文件中添加了两句权限声明:
因为涉及用户设备安全性,因此必须在该文件加入权限声明,否则程序会崩溃。
使用该机制,用户主要在两方面得到了保护:
在低于6.0系统的设备安装程序,在安装界面会给出提醒,让用户知晓程序申请了哪些权限,从而决定是否安装该程序。 用户可以随时在应用程序管理界面查看任意一个程序的权限申请情况,保证应用程序不会滥用权限。该权限设计思路:用户如果认可你所申请的权限,那就安装,否则就拒绝安装。
运行时权限:用户不需要在安装应用程序时一次性赋予所有申请权限,而是在运行的时候再对某一权限的申请进行授权。这样使得用户可以拒绝某一权限,但仍然可以继续使用该应用。
Android权限分为三类:
普通权限(不会直接威胁到用户的安全和隐私的权限,如设备网络状态和开机自启动等,由系统自动授权) 危险权限(可能会触及用户隐私和对设备的安全性造成影响,如设备联系人信息和地理位置等,由用户手动点击授权) 特殊权限(用的很少)除了危险权限之外,剩余的都是普通权限。下表列出了Android中的所有危险权限,一共9组24个权限。
权限组名 | 权限名 |
---|---|
CALENDAR | READ_CALENDAR WRITE_CALENDAR |
CAMERA | CAMERA |
CONTACTS | READ_CONTACTS WRITE_CONTACTS GET_ACCOUNTS |
LOCATION | ACCESS_FINE_LOCATION - ACCESS_COARSE_LOCATION - |
MICROPHONE | RECORD_AUDIO |
PHONE | READ_PHONE_STATE CALL_PHONE READ_CALL_LOG WRITE_CALL_LOG ADD_VOICEMAIL USE_SIP - PROCESS_OUTGOING_CALLS |
SENSOR | BODY_SENSORS |
SMS - | SEND_SMS RECEIVE_SMS READ_SMS RECEIVE_WAP_PUSH RECEIVE_MMS |
STORAGE | READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE |
附 Android完整权限列表
7.2.2 在程序运行时申请权限新建项目
RuntimePermissionTest
,修改activity_main.xml
布局文件,如下所示:
接着修改
MainActivity
中的代码,如下所示:
package com.example.runtimepermissiontest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try{
Intent intent = new Intent(Intent.ACTION_CALL);
//Intent.ACTION_CALL是一个系统内置的打电话动作
intent.setData(Uri.parse("tel:10086"));
//指定了协议是tel,号码是10086
startActivity(intent);
}catch (SecurityException e){
e.printStackTrace();
}
}
});
}
}
接下来修改
AndroidManifest.xml
文件
<application
...
运行程序报错是因为权限被禁止所导致,因为6.0系统及以上系统在使用危险权限时都必须进行运行时权限处理。
修改
MainActivity
的代码,来修复这个问题,如下所示:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = findViewById(R.id.make_call);
makeCall.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);
//如果没有授权就调用 ActivityCompat.requestPermissions()方法,第二个参数是权限名,第三个是请求码,唯一值就行
} 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, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//同意则拨打电话
call();
} else {
//否则放弃操作,弹出失败提示
Toast.makeText(this, "You denied th permission", Toast.LENGTH_SHORT).show();
}
break;
}
}
}
7.3 访问其他程序中的数据
内容提供器的使用方法有两种,一种是使用现在的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们的程序提供外部访问接口。
7.3.1ContentResolver
的基本用法
首先通过
Context
中的getContextResolver()
方法获取该类的实例,然后调用该类提供的一系列方法用于对数据进行CRUD操作。其中:
insert() 方法:用于添加数据
update() 方法:用于更新数据
delete() 方法:用于删除数据
query() 方法:用于查询数据
内容URI
这些方法接收一个Uri参数,这个参数被称为内容URI,用于给内容提供器中的数据建立起唯一的标识符。它由两部分组成:
authority:用于区分不同的应用程序,一般用程序包名命名,比如某个程序的包名是com.example.app
,那么该程序的authority可以命名为com.example.app.provider
。
path:用于区分同一个应用程序中的不同表,通常添加到authority的后面
此外,在前面还需要加上协议声明。因此,内容URI的最标准格式写法如下:content://com.example.app.provider/table
另外,可以在后面加上一个id,表示访问id为该值的数据,如下所示标准访问id为1的数据:
content://com.example.app.provider/table/1
我们还可以使用通配符的方式来分别匹配这两种格式的内容URI,规则如下:
*:表示匹配任意长度的任意字符。
#:表示匹配任意长度的数字。
所以一个能匹配任意表的内容URI格式可以写成:
别忘了,还需要在content://com.example.app.provider
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{Manifest.permission.READ_CONTACTS}, 1);
//如果没有授权就调用 ActivityCompat.requestPermissions()方法,第二个参数是权限名,第三个是请求码,唯一值就行
} else {
readContacts();
}
}
private void readContacts() {
Cursor cursor = null;
try {
//查询联系人数据,ContactsContract.CommonDataKinds.Phone.CONTENT_URI封装了联系人数据表的URI
getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
if (cursor.moveToNext()) {
//获取联系人姓名,常量ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME对应联系人姓名
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
//获取联系人手机号码,常量ContactsContract.CommonDataKinds.Phone.NUMBER对应联系人手机号码
String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(displayName + "\n" + number);
}
//通知刷新一下ListView
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 th permission", Toast.LENGTH_SHORT).show();
}
break;
}
}
}
声明读取系统联系人的权限,如下所示:AndroidManifest.xml
<application
...
7.4 创建自己的内容提供器
7.4.1 创建内容提供器的步骤
新建一个类去继承
ContentProvider
的方式来创建一个自己的内容提供器,然后重写该类的6个抽象方法,代码如下所示:
class MyProvider extends ContentProvider{
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
因为所有的CRUD操作都一定要匹配到相应的内容URI格式才能进行的,而我们当然不可能向
UriMatch
中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,因此保证了隐私数据不会泄露出去。
7.4.2 实现跨程序数据共享
打开上一章
DatabaseTest
项目,创建一个内容提供器,代码如下所示:
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);
}
public DatabaseProvider() {
}
@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.dir/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.dir/vnd.com.example.databasetest.provider.category";
}
return null;
}
@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 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);//将内容URI权限后的部分以“/”进行分割并放入一个字符串列表,第0个位置是路径,第1个位置是id
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 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);
db.update("Category", values, "id =?", new String[]{categoryId});
break;
}
return updatedRows;
}
}
删除并重新安装
DatabaseTest
程序,接着新建一个新项目ProviderTest
。先修改activity_main
布局文件,如下所示:
然后修改
MainActivity
中的代码,如下所示:
public class MainActivity extends AppCompatActivity {
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", 22.85);
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
}
});
Button queryData = 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"));
float prices = cursor.getFloat(cursor.getColumnIndex("prices"));
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 prices is " + prices);
}
cursor.close();
}
}
});
Button updateData = 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 = findViewById(R.id.delete_data);
updateData.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);
}
});
}
}
7.5 Git时间——版本控制进阶
略
7.6 小结和点评在本章中,我们一开始了解了Android的权限机制,并且学会了如何在6.0以上的系统使用运行时权限,然后又学习了内容提供器的相关内容,以实现跨程序数据共享的功能。
作者:alanliang1998