跨程序共享数据–探究内容提供者
内容提供器简介
内容提供器(Content Provider)主要用于在不同应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序的数据,同时还保证被访问数据的安全性。目前,使用内容提供器时Android实现跨程序共享数据的标准方式
不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选择只对那一部分数据进行共享,是从而来保证我们程序中的隐私数据不会有泄漏风险
运行时权限
Android开发团队在Android6.0系统中引用了运行时权限这个功能,从而更好地保护了用户的安全和隐私
Android权限机制详解
在Android6.0系统中加入运行时权限功能。也就是说,用户不需要再安装软件的时候一次性授权所有申请的权限,而是可以在软件的使用过程中再对某一项权限申请进行授权。比如一款相机应用在运行时申请了地理位置定位权限,就算我拒绝了这个权限,但是我应该仍然可以使用这个应用的其他功能,而不是像之前那样直接无法安装它
Android现在讲所有的权限归成两类,一类是普通权限,一类是危险权限。普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,而不需要用户再去手动操作了。危险权限则表示那些可能会触及用户隐私,或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须要有用户手动点击授权才可以,否则程序就无法使用相应功能
Android中所有的危险权限:
注意:表中每个危险权限都属于一个权限组,我们进行运行时权限处理时使用的是权限名,但是用户一旦同意授权了,那么该权限所对应的权限组总所有的其他权限也会同时被授权
访问http://developer.android.com/reference/android/Manifest.permission.html可以查看Android系统中完整的权限列表
在程序运行时申请权限
新建一个RuntmePermissionTest项目,修改activity_main.xml布局文件,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.demo.runtimepermissiontest.MainActivity">
<Button
android:id="@+id/make_call"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Make Ccall"
android:textAllCaps="false" />
</LinearLayout>
修改MAinActivity中的代码,如下所示:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
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, @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 the permission" , Toast.LENGTH_SHORT).show();
}
break;
default:
}
}
}
运行时权限的核心就是在程序运行过程中由用户授权我们去执行某些危险操作,程序时不可以擅自做主去执行这些危险操作的。因此,第一步就是要先判断用户是不是已经给我们授权了,借助的是(ContextCompat.checkSelfPermission())方法。checkSelfPermission()方法接收两个参数,第一个参数是Context,第二个参数是具体的权限名,然后使用方法的返回值和PackageManager.PWEMISSION_GRANTED做比较,相等就说明用户已经授权,不等就表示用户没有授权
如果已经授权,留执行打电话的逻辑,这里我们把拨打电话的逻辑封装到call()方法当中。如果没有授权的话,则需要调用ActivityCompat.requestPermission()方法来向用户申请授权,requestPermission()方法接收3个参数,第一个参数要求是Activity的实例,第二个参数是一个String数组,我们把要申请的权限名放在数组中即可,第三个参数是请求码,只要是为一直就可以,这里传入1
调用完requestPermission()方法之后,系统会弹出一个权限申请的对话框,然后用户可以选择同一或拒绝我们的权限申请,无论是那种结果,最终都会调用onRequestPermissionsResult()方法中,而授权的结果会封装在grantResults参数当中。这里我们只需要判断一下授权结果,如果用户同一的话就调用call()方法来拨打电话,如果用户是拒绝的话我们只能放弃操作,并且弹出一条失败提示
访问其他程序的数据
内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供其给我们程序提供外部访问接口。
ContentProvider的基本用法
不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。内容URI给内容提供器中的数据建立了唯一的标识符,它主要有两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。
在得到了内容Uri后,我们还需要将它解析成Uri对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:
Uri uri = Uri.parse("content://com.demo.app.provider/table");
只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象了
读取系统联系人
新建一个ContactsTest项目,修改activity_main.xml中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.demo.contactstest.MainActivity">
<ListView
android:id="@+id/contacts_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
MAinActivity中的代码,如下所示:
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:
}
}
}
ContactsContract.CommonDataKinds.Phone已经做好了封装,提供了一个CONTENT_URI常量,而这个常量就是使用Uri.parse()方法解析出来的结果。接着我们对Cursor对象进行遍历,将联系人姓名和手机号这些数据逐个取出,联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,联系人手机号这一列对应的常量是ContactsContract.CommonDataKinds.Phone.NUMBER。这两个数据都取出之后,将它们进行拼接,并且在中间加上换行符,然后将拼接后的数据添加到ListView的数据源里,并通知刷新一下ListView。最后将Cursor对象关闭掉
修改AndroidManiFest.xml中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.demo.contactstest">
<uses-permission android:name="android.permission.READ_CONTACTS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
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>
创建自己的内容提供器
创建内容提供器的步骤
新建一个类去继承ContentProvider的方式来创建一个自己的内容提供器。ContentProvider类中有6个抽象方法,我们再使用子类继承它的时候,需要将者6个方法全部重写,新建MyProvider继承自 ContentProvider,代码如下所示:
public class MyProvider extends ContentProvider { @Override public boolean onCreate() { return false; } @Nullable @Override public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) { return null; } @Nullable @Override public String getType(Uri uri) { return null; } @Nullable @Override public Uri insert(Uri uri, ContentValues contentValues) { return null; } @Override public int delete(Uri uri, String s, String[] strings) { return 0; } @Override public int update(Uri uri, ContentValues contentValues, String s, String[] strings) { return 0; } }
1、onCreate()
初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回true表示内容提供器初始化成功,返回false则表示失败。注意,只有当存在ContentResolver尝试访问我们程序中的数据时,内容提供器才会被初始化
2、query()
从内容提供器中查询数据
3、insert()
向内容提供器中添加一条数据
4、update()
更新内容提供器中已有的数据
5、delete()
向内容提供器中删除数据
6、getType()
根据传入的内容URI来返回相应的MIME类型
标准的内容URI写法:
content://com.demo.app.provider/table :这就表示调用方期望访问的是com.demo.app这个应用中的table表中的数据。除此之外我们还可以在这个内容URI的后面加上一个id,如下所示:
content://com.demo.app.provider/table/1
以路径结尾表示期望访问该表中所有的数据,以id结尾就表示期望访问该表中拥有相应id的数据。我们可以使用通配符的方式来分别匹配这两种格式的URI,规则如下:
* *:表示匹配任意长度的任意字符
* #:表示匹配任意长度的数字
一个能匹配任意表的内容URI格式就可以写成:
content://com.demo.app.provider/*
一个能匹配任意一行数据的内容URI格式就可以写成:
content://com.demo.app.provider/table1/#
接着,我们再借助UriMatcher,这个类就可以轻松地实现匹配内容URI的功能。UriMatcher中提供了一个addURI()方法,这个方法接收3个参数,可以分别把authority、path和一个自定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的定义代码,利用这个代码,我们就可以判断出调用方期望访问的是那张表中的数据了。修改MyProvider中的代码,如下所示:
public class MyProvider extends ContentProvider {
public static final int TABLE1_DIR = 0;
public static final int TABLE1_ITEM = 1;
public static final int TABLE2_DIR = 2;
public static final int TABLE2_ITEM = 3;
private static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.demo.runtimepermissiontest.provider" , "table1" , TABLE1_DIR);;
uriMatcher.addURI("com.demo.runtimepermissiontest.provider" , "table1/#" , TABLE1_ITEM);;
uriMatcher.addURI("com.demo.runtimepermissiontest.provider" , "table2" , TABLE2_DIR);;
uriMatcher.addURI("com.demo.runtimepermissiontest.provider" , "table2/#" , TABLE2_ITEM);;
}
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
//查询table1表中的所有数据
break;
case TABLE1_ITEM:
//查询 table1表中的单条数据
break;
case TABLE2_DIR:
//查询table2表中的所有数据
break;
case TABLE2_ITEM:
//查询table2表中的单条数据
break;
default:
break;
}
...
}
...
}
getType()方法:是所有的内容提供器都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成,Android对着3部分做了如下格式规定:
* 必须以vnd开头
* 如果内容URI一路径结尾,则后接android.cursor.dir/,如果内容URI以id结尾,则后接android.cursor.item/
* 最后接上vnd.<authority>.<path>
实现getType()方法总的逻辑,代码如下:
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case TABLE1_DIR:
return "vnd.android.cursor.dir/vnd.com.demo.runtimepermissiontest.provider.table1";
case TABLE1_ITEM:
return "vnd.android.cursor.item/vnd.com.demo.runtimepermissiontest.provider.table1";
case TABLE2_DIR:
return "vnd.android.cursor.dir/vnd.com.demo.runtimepermissiontest.provider.table2";
case TABLE2_ITEM:
return "vnd.android.cursor.item/vnd.com.demo.runtimepermissiontest.provider.table2";
}
return null;
}
所有的CRUD操作都一定要匹配到相应的内容URI格式才能进行的,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了
实现跨程序数据共享
在DataBaseTest项目上继续开发,通过内容提供器来给它加入外部访问接口。新建一个ContentProvider,将内容提供器命名为DatabaseProvider.authority指定为com.demo.databasetest.provider,Exported属性表示是否允许外部程序访问我们的内容提供器,Enable属性表示是否启用这个内容提供器,将这两个属性都勾中,点击finish完成创建
接着我们修改DatabaseProvider总的代码,如下所示:
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.demo.database.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.demo.databasetest. provider.book";
case BOOK_ITEM:
return "vnd.android.cursor.item/vnd.com.demo.databasetest. provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.demo.databasetest. provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.demo.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);
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);
updatedRows = db.update("Category", values, "id = ?", new String[] { categoryId });
break;
default:
break;
}
return updatedRows;
}
}
内容提供器一定要在AndroidManifest.xml文件中注册才可以使用
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.demo.databasetest">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
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:name=".DatabaseProvider"
android:authorities="com.demo.databasetest.provider"
android:enabled="true"
android:exported="true"></provider>
</application>
</manifest>
标签对内容提供器进行注册。android:name属性指定了DatabaseProvider的类名,android:authorities属性指定了DatabaseProvider的authority,而enable和exported属性则是根据我们刚才勾选的状态自动生成的,这里表示DatabaseProvider被其他应用程序进行访问
新建一个名为ProviderTest的项目,修改activity_main.xml中的代码,如下所示:
<?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_data"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add To Book" />
<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" />
</LinearLayout>
修改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 = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//添加数据
Uri uri = Uri.parse("content://com.demo.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 view) {
//查询数据
Uri uri = Uri.parse("content://com.demo.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.demo.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.demo.databasetest.provider/book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}