目前所有的信息数据都只能在当前应用程序中访问,不能被其他应用程序访问,Antroid 提供了内容提供器(Content Provider)机制,允许一个程序访问另一个程序中的数据,同时保证被访问数据的安全性,内容提供器使用时会用到运行时权限功能
运行时权限
Android 6.0 系统中引用了运行时权限这个功能,从而更好保护用户安全和隐私,下列是危险权限,一共9组24个权限,剩下的都是普通权限
权限组名 | 权限名 |
---|---|
CALENDAR(日历) | READ_CALENDAR WRITE_CALENDAR |
CAMERA(相机) | CAMERA |
CONTACTS(联系人) | READ_CONTACTS WRITE_CONTACTS GET_CONTACTS |
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 |
SENSORS(短信) | BODY_SENSORS |
SMS(传感器) | SEND_SMS RECEIVE_SMS READ_SMS RECEIVE_WAP_PUSH RECEIVE_MMS |
STORAGE(存储卡) | READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE |
如果是这张表中的权限,就需要进行运行时权限处理,如果不是就可以在 AndroidManifest.xml 文件中添加权限的声明
注意:一旦用户授权的其中的一个权限,那么整个权限组中的其他权限也会同时被授权
申请运行时权限
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content_provider);
Button button = (Button) findViewById(R.id.content_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (ContextCompat.checkSelfPermission(ContentProvider.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(ContentProvider.this, new String[]{Manifest.permission.CALL_PHONE}, 1);
}else {
call();
}
}
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
call();
}else {
Toast.makeText(this, "未授权", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
private void call(){
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}
运行时权限的核心就是在程序运行的过程中由用户去执行危险操作,第一步通过 ContextCompat.checkSelfPermission() 方法判断用户是不是已经授权过了,接收两个参数,第一个是一个上下文 Context,第二个是具体权限名,这里是打电话的权限就是 Manifest.permission.CALL_PHONE,最后与 PackageManager.PERMISSION_GRANTED 作比较,相等表示已经授权
如果未授权,需要调用 ActivityCompat.requestPermissions() 方法向用户去申请授权,接收3个参数,第一个是 Activity 实例,第二个是一个 String 数组,数组里放的是申请的权限名,第三个是请求码,请求码至于要唯一就行
调用完 ActivityCompat.requestPermissions() 后,系统会弹出一个授权申请的对话框,用户可以选择同一或拒绝申请,无论那种结果,都会回掉到 onRequestPermissionsResult() 方法中,授权结果封装在 grantResults 中,判断一下授权结果,用户授权同意就调用 call() 方法进行打电话,否则弹出一条失败信息
别忘了在 AndroidManifest.xml 文件中添加打电话的权限
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.helloworld">
<uses-permission android:name="android.permission.CALL_PHONE" />
.......
</manifest>
ContentResolver 基本用法
内容提供器有两种用法:一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给程序的数据提供外部访问的接口
对于每一个应用程序来说想要访问内容提供器中的共享数据时,要借助 ContentResolver 类,可以通过 Context 中的 getContentResolver() 方法来获取该类的实例,ContentResolver 中提供了一系列方法对数据进行 CRUD 操作,insert() 方法用于添加数据,update() 方法用于更新数据,delete() 方法用于删除数据,query() 方法用于查询数据,这些方法接收一个 Uri 参数,内容Uri 给内容提供器中的数据建立了唯一的表示符,由两部分组成:authority 和 path,authority 是用于对不同的应用程序作区分,path 则是用于对统一应用程序中的不同表做区分通常添加到 authority后面,还需要在头部加上协议声明 content://<packagename>/<tablename>
在有了Uri 字符串之后要将它解析成 Uri 对象
Uri uri = Uri.parse("content://com.example.helloworld.provider/book");
Cursor cursor = getContentResolver.query(uri, projection, selection, selectionArgs, sortOrder);
query()方法参数 | 描述 |
---|---|
uri | 指定查询某个应用程序下的某张表 |
projection | 指定查询的列名 |
selection | 指定where的约束条件 |
selectionArgs | 为where的占位符提供具体数值 |
sortOrder | 指定查询结果的排序方式 |
查询完后返回一个 Cursor 对象,就可以从这个对象中读出数据
if(cursor != null){
while(cursor.moveToNext()){
String column1 = cursor.getString(cursor.getColumnIndex("column1"));
int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
}
cursor.close();
}
添加
ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);
将待添加的数据组装到 ContentValues 中,然后调用 insert() 方法将 Uri 和 ContentValues 作为参数传入即可
更新
ContentValues values = new ContentValues();
values.put("column1", "text1111");
values.put("column2", 2;
getContentResolver().update(uri, values, "column1=? and column2=?", new String[] {"text", "1"});
不传如约束条件 selection 和 selectionArgs 将会将所有的数据进行更新
删除
getContentResolver().delete(uri, values, "column2=?", new String[] {"1"});
同样不传入约束添加将会删除所有数据
读取联系人
先在布局文件中建立一个 ListView,将读出的联系人在 ListView 中显示
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="match_parent">
<ListView
android:id="@+id/contacts_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
同样在 onCreate() 方法中先获取 ListView 控件的实例,并添加上适配器,然后调用运行时权限处理逻辑,用户授权后调用 readContacts() 方法来读取联系人
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_content_provider);
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(ContentProvider.this, new String[]{Manifest.permission.READ_CONTACTS}, 1);
}else {
readContacts();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode){
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
readContacts();
}else {
Toast.makeText(this, "未授权"+grantResults.length+"::"+grantResults[0], Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
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();
}
}
}
readContacts() 方法中使用了 getContentResolver() 方法获得 ContentResolver 对象,又调用了 ContentResolver 中的 query() 方法查询系统的联系人数据,查询出来是一个 Cursor 对象,接着对这个 Cursor 对象进行遍历,将联系人姓名和电话号码取出添加到 ListView 的数据源里,并通知刷新 ListView,最后关闭 Cursor 对象
最后对系统读取联系人的权限在 AndroidManifest.xml 文件中进行声明
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.helloworld">
<uses-permission android:name="android.permission.READ_CONTACTS" />
.......
</manifest>
创建自定义内容提供器
通过创建一个类去继承 ContentProvider 的方法来创建一个自定义的内容提供器, ContentProvider 有6个抽象方法,需要全部重写
public class MyProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
return 0;
}
}
- onCreate()
初始化内容提供器的时候调用,通常用于对数据库的创建和升级等操作,返回 true 表示内容提供器初始化成功,false 表示失败,注意,只有 ContentResolver 尝试访问程序中的数据时,内容提供器才会被初始化 - query()
从内容提供器中查询数据,使用 uri 参数来确定查询那张表,projection 参数用于确定查询那些列,selection 和 selectionArgs 参数用于约束查询条件,sortOrder 参数用于对查询结果排序,查询结果放在 Cursor 对象中返回 - update()
更新内容提供器中是数据,使用 uri 参数来确定更新那一张表中的数据,新数据保存在 values 参数中,selection 和 selectionArgs 参数用于约束更新的条件,返回受影响的行数 - insert()
向内容提供器中添加一条数据,使用uri参数来确定要添加到那张表,待添加数据保存在 values 参数中,添加完成后返回一个用于表达这条新数据的 URI - delete()
从内容提供器中删除数据,使用uri 参数来确定删除那一张表中的数据,selection 和 selectionArgs 参数用于约束删除的条件,返回被删除的行数 - getType()
根据传入的内容URI来返回相应的MIME类型
可以看到每个方法都有一个 Uri 参数,这个参数就是调用 ContentResolver 的增删改查方法时传递过来的,内容URI 的格式有两种,一种是以路径结尾表示访问该表中的所有数据,一种是以id 结尾,表示访问该表中对应id 的数据
UriMatcher 这个类可以轻松实现匹配内容URI的功能,UriMatcher 中提供了一个 addURI() 方法,这个方法接受3个参数,第一个是 authority,第二个是 path,第三个是 一个自定义的代码,再调用 UriMatcher 的 match() 方法,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri 对象所对应的自定义代码
public static final int BOOK_DRI = 0;
public static final int BOOK_ITEM = 1;
private static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.example.helloworld.provider", "book", BOOK_DRI);
uriMatcher.addURI("com.example.helloworld.provider", "book/#", BOOK_ITEM);
}
//......
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
switch (uriMatcher.match(uri)){
case BOOK_DRI:
//查询表中所有的数据
break;
case BOOK_ITEM:
//查询表中单条数据
break;
}
return null;
}
//.......
这样就可以很方便的判断用户要的是什么数据,不用拆分 Uri 再用一大堆 if 进行判断,而且能够被访问的数据都放进了uriMatcher 对象里,不用判断哪些是合法请求,哪些是非法请求。在静态代码块中创建了 UriMatcher 的实例,并调用 addURI() 方法将期望匹配的内容URI格式传递进去,当调用 query() 方法的时候,通过 UriMatcher 的 match() 方法就可以对传入的Uri对象进行匹配,如果配对成功就返回相应的自定义代码,就可以知道用户期望访问的数据了,inster()、update()、delete() 都是一样的判断
getType() 方法是所有内容提供器都必须提供的一个方法,用于获取 Uri 对象所对应的 MIME 类型,一个内容URI所对应的 MIME 字符串由3部分组成,第一必须由 vnd 开头,第二如果是以路径结尾,则后接 android.cursor.dir/,如果是以 id 结尾,则后接 android.cursor.item/ 第三最后接上 vnd.<packagename>.<path>
所以 content://com.example.helloworld.provider/book
对应的 MIME 类型是vnd.android.cursor.dir/vnd.com.example.helloworld.provider/book
content://com.example.helloworld.provider/book/1
对应的 MIME 类型是vnd.android.cursor.item/vnd.com.example.helloworld.provider/book
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)){
case TABLE_DRI:
return "vnd.android.cursor.dir/vnd.com.example.helloworld.provider/book";
case TABLE_ITEM:
return "vnd.android.cursor.item/vnd.com.example.helloworld.provider/book";
}
return null;
}
前面查询数据库可以使用 LitePal 的方法,但是 LitePal 查询结果是一个 Book 的实体对象,而 ContentProvider 返回的是一个 Cursor 对象,需要进行转变,就不如直接使用继承 SQLiteOpenHelper 方法,这样查询出来的直接就是一个 Cursor 对象。
public class MyProvider extends ContentProvider {
public static final int TABLE_DRI = 0;
public static final int TABLE_ITEM = 1;
private static UriMatcher uriMatcher;
private MyDatabaseHelper myDatabaseHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.example.helloworld.provider", "book", TABLE_DRI);
uriMatcher.addURI("com.example.helloworld.provider", "book/#", TABLE_ITEM);
}
@Override
public boolean onCreate() {
myDatabaseHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 5);
return true;
}
// 查询数据
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] prjection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Log.d("zwt", "进入查询"+uriMatcher.match(uri));
SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)){
case TABLE_DRI:
//查询表中所有的数据
cursor = db.query("Book", prjection, selection, selectionArgs, null, null, sortOrder);
Log.d("zwt", "TABLE_DRI:cursor是否为空:"+String.valueOf(cursor==null));
break;
case TABLE_ITEM:
//查询表中单条数据
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", prjection, "id=?", new String[]{bookId}, null, null, sortOrder);
Log.d("zwt", "TABLE_ITEM:cursor是否为空:"+String.valueOf(cursor==null));
break;
}
return cursor;
}
// 插入数据
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
Log.d("zwt", "进入插入数据"+uriMatcher.match(uri));
SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)){
case TABLE_DRI:
case TABLE_ITEM:
long newBookId = db.insert("Book", null, contentValues);
uriReturn = Uri.parse("content://com.example.helloworld.provider/book/" + newBookId);
break;
}
return uriReturn;
}
// 删除数据
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.d("zwt", "进入删除数据"+uriMatcher.match(uri));
SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
int deleteRows = 0;
switch (uriMatcher.match(uri)){
case TABLE_DRI:
deleteRows = db.delete("Book", selection, selectionArgs);
break;
case TABLE_ITEM:
String bookId = uri.getPathSegments().get(1);
deleteRows = db.delete("Book", "id=?", new String[]{bookId});
break;
}
return deleteRows;
}
// 更新数据
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs) {
Log.d("zwt", "进入更新数据"+uriMatcher.match(uri));
SQLiteDatabase db = myDatabaseHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)){
case TABLE_DRI:
updatedRows = db.update("Book", contentValues, selection, selectionArgs);
break;
case TABLE_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", contentValues, "id=?", new String[]{bookId});
break;
}
return updatedRows;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)){
case TABLE_DRI:
return "vnd.android.cursor.dir/vnd.com.example.helloworld.provider/book";
case TABLE_ITEM:
return "vnd.android.cursor.item/vnd.com.example.helloworld.provider/book";
}
return null;
}
}
最后,内容提供器一定要在 AndroidManifest.xml 文件中进行注册才能使用
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.helloworld">
<application
android:name="org.litepal.LitePalApplication"
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">
......something code
<provider
android:authorities="com.example.helloworld.provider"
android:name="com.example.contentprovider.MyProvider"
android:enabled="true"
android:exported="true">
</provider>
</application>
</manifest>
android:enabled=“true” 该属性表明了该content provider是否可以被实例化
android:exported=“true” 该属性指示了content provider是否可以被其他应用程序使用
建立一个新的项目,创建4个按钮用于增删改查
private String newId;
addDate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Uri uri = Uri.parse("content://com.example.helloworld.provider/book");
ContentValues values = new ContentValues();
values.put("name", "33333");
values.put("author", "ccc");
values.put("price", 110.90);
values.put("press", "Unknow");
Uri newUri = getContentResolver().insert(uri, values);
newId = newUri.getPathSegments().get(1);
Toast.makeText(MainActivity.this, "添加数据成功", Toast.LENGTH_SHORT).show();
}
});
deleteDate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Uri uri = Uri.parse("content://com.example.helloworld.provider/book/"+newId);
getContentResolver().delete(uri, null, null);
Toast.makeText(MainActivity.this, "删除数据成功", Toast.LENGTH_SHORT).show();
}
});
updateDate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Uri uri = Uri.parse("content://com.example.helloworld.provider/book/"+newId);
ContentValues values = new ContentValues();
values.put("press", "China");
getContentResolver().update(uri, values, null, null);
Toast.makeText(MainActivity.this, "更新数据成功", Toast.LENGTH_SHORT).show();
}
});
queryDate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Uri uri = Uri.parse("content://com.example.helloworld.provider/book");
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if(cursor!=null){
String name = null;
String author = null;
double price = 0;
while(cursor.moveToNext()){
name = cursor.getString(cursor.getColumnIndex("name"));
author = cursor.getString(cursor.getColumnIndex("author"));
price = cursor.getDouble(cursor.getColumnIndex("price"));
}
cursor.close();
Toast.makeText(MainActivity.this, "书名:"+name+"作者::"+author+"价格::"+price, Toast.LENGTH_SHORT).show();
}
}
});
可以看到在 test 这个应用中可以操作 helloworld 应用中的数据