在上一节《Android高级应用1----Service和AIDL》中有介绍过AIDL,作为服务进程间数据访问的接口,而对于像Android自带的SQLite数据库,如果其他的应用程序想要访问该数据库,进行CRUD,同样也需要数据接口,那么这个就是通过ContentProvider来实现,这个虽然在开发过程中,并不会用到太多,大多数是通过后台提供的数据接口来处理,但别忘记,ContentProvider也是Android的4大组件之一。
1、ContentProvider
1个应用程序想要为外界提供数据接口,就必须要使用ContentProvider对外提供
Authorities是非常重要的一个参数,这个是数据接口Uri的重要组成部分,也是是否允许对方访问数据的凭证之一。
public class MyContentProvider extends ContentProvider {
public MyContentProvider() {
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Implement this to handle requests to delete one or more rows.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public String getType(Uri uri) {
// TODO: Implement this to handle requests for the MIME type of the data
// at the given URI.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO: Implement this to handle requests to insert a new row.
throw new UnsupportedOperationException("Not yet implemented");
}
//做数据库的初始化
@Override
public boolean onCreate() {
// TODO: Implement this to initialize your content provider on startup.
MySQLiteOpenHelper helper = new MySQLiteOpenHelper(getContext());
database = helper.getReadableDatabase();
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// TODO: Implement this to handle query requests from clients.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO: Implement this to handle requests to update one or more rows.
throw new UnsupportedOperationException("Not yet implemented");
}
}
创建了ContentProvider,会提供4个重要的增删改查方法,需要传入Uri。
2、ContentResolver
如果其他应用程序,想要访问主App的数据,就得通过ContentResolver
来实现;ContentResolver能够实现增删改查4项操作,那么就可以通过主App提供的数据接口,来进行远程数据库的CRUD操作。
那么现在首先是需要在主App中创建数据库,在创建数据库以及表的时候,就需要使用传统的SQLiteOpenHelper来创建,具体就不在此赘述,很基础的问题。
创建好数据库之后,就需要在其他应用程序中,通过数据接口访问主App的数据库,进行数据的操作。
(1)添加数据
case R.id.btn_add:
//添加数据
ContentValues values = new ContentValues();
//要与数据库中的表的字段一致
values.put("name",name);
values.put("age",age);
values.put("sex",gender);
resolver.insert(Uri.parse("content://com.lay.provider"),values);
break;
ContentResolver的insert方法,是需要传入两个参数,一个参数是Uri,是统一资源标识符,也就是数据接口,格式为:content://authorities(在AndroidMainfest文件中注册的);第二个参数为新增的数据信息。
2020-06-05 16:40:28.998 9476-9489/com.example.providerdemo E/TAG: 其他App调用了添加方法
在ContentProvider
方的insert方法中,也同样存在两个参数Uri和ContentValues,这和ContentResolver是对等的,因此values也是ContentResolver
的values。
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO: Implement this to handle requests to insert a new row.
Log.e("TAG","其他App调用了添加方法");
//执行真正的数据插入操作
database.insert(StudentTable.TAB_NAME,null,values);
//将插入的id与Uri拼接
return ContentUris.withAppendedId(uri,id);
}
(2)查询数据
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// TODO: Implement this to handle query requests from clients.
Cursor cursor = database.query(StudentTable.TAB_NAME, projection, selection, selectionArgs,
null, null, sortOrder);
return cursor;
}
在其他应用程序中查询数据时,可以使用SimpleCursorAdapter
来作为ListView的适配器显示数据库中某表的全部数据。
case R.id.btn_query:
//查询
Cursor cursor = resolver.query(contentUri, null, null, null, null);
SimpleCursorAdapter adapter =
new SimpleCursorAdapter(this,
R.layout.layout_student_info_item,
cursor,
new String[]{"_id","name","age","sex"},
new int[]{R.id.tv_number,R.id.tv_name,R.id.tv_age,R.id.tv_sex},
CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
//设置ListView适配器
lv_show.setAdapter(adapter);
break;
(3)删除数据
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Implement this to handle requests to delete one or more rows.
// throw new UnsupportedOperationException("Not yet implemented");
int id = database.delete(StudentTable.TAB_NAME, selection, selectionArgs);
return id;
}
当删除数据成功之后,会返回一个int类型的值,当int类型的值大于0时,代表删除成功。
case R.id.btn_delete:
String ids = et_number.getText().toString();
//占位符
int result = resolver.delete(contentUri, "name = ?", new String[]{ids});
Toast.makeText(MainActivity.this,"成功删除"+result,Toast.LENGTH_SHORT).show();
break;
(4)更新数据
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO: Implement this to handle requests to update one or more rows.
// throw new UnsupportedOperationException("Not yet implemented");
int update = database.update(StudentTable.TAB_NAME, values, selection, selectionArgs);
return update;
}
ContentValues 指的是要更改的字段以及字段值。
case R.id.btn_update:
//update table name = xx,age =xx where id = ?;
ContentValues values1 = new ContentValues();
values1.put("name","小李");
values1.put("age",23);
int update = resolver.update(contentUri,
values1, "name = ?", new String[]{"xx"});
Toast.makeText(MainActivity.this,"更新成功"+update,Toast.LENGTH_SHORT).show();
break;
3、Uri
在上面的例子中,使用的Uri就是content://authorities
,但是如果使用这种Uri数据接口是非常不安全的,第三方如果获取到authorities
,就可以无限攻击我们的数据库,这样我们的数据没有安全性的保证,因此在定义数据接口时,要定义约定好的接口,再者就是主App,要对第三方访问的数据接口做判断,是否符合通信标准,这就需要使用UriMatcher
。
(1)UriMatcher
UriMatcher类的主要作用就是在ContentProvider创建时,制定好匹配规则,当其他App调用该接口时,需要去验证是否匹配,根据uri给出相应的数据。
在ContentProvider创建时,就是onCreate方法
//如果没有匹配到,就是NO_MATCH
matcher = new UriMatcher(UriMatcher.NO_MATCH);
//制定规则
matcher.addURI("com.lay.provider","insert",INSERT_CODE);
在这里制定了一个插入操作的uri,也就是说在执行插入操作的时候,必须要符合这个规则,才可以将数据插入数据库。
@Override
public Uri insert(Uri uri, ContentValues values) {
Log.e("TAG","其他App调用了添加方法");
long id = 0;
//与第三方访问App传过来的uri匹配
int code = matcher.match(uri);
switch (code){
case INSERT_CODE:
id = database.insert(StudentTable.TAB_NAME, null, values);
break;
}
return ContentUris.withAppendedId(uri,id);
}
插入操作,使用新制定的uri。
case R.id.btn_add:
String name = et_name.getText().toString();
String age = et_age.getText().toString();
//添加数据
ContentValues values = new ContentValues();
values.put("name",name);
values.put("age",age);
values.put("sex",gender);
Uri parse = Uri.parse("content://com.lay.provider/insert");
Uri uri = resolver.insert(parse, values);
long id = ContentUris.parseId(uri);
Toast.makeText(MainActivity.this,"插入成功"+id,Toast.LENGTH_SHORT).show();
break;
(2)解析Uri
先看代码吧
case R.id.btn_add:
resolver.insert(
Uri.parse("content://com.lay.provider/anyway?name=张三&age=23&sex=男"),
new ContentValues());
break;
在插入操作的时候,主机名是必须的,但是path并没有什么特殊的要求,随便写的,在后边有一串类似于http的get请求的参数,创建了一个空的ContentValues;
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO: Implement this to handle requests to insert a new row.
Log.e("TAG","其他App调用了添加方法");
long id = 0;
//与第三方访问App传过来的uri匹配
if(values.size() > 0 ){
id = database.insert(StudentTable.TAB_NAME,null,values);
}else{
//解析Uri
//content://com.lay.provider/anyway?name=张三&age=23&sex=男
String authority = uri.getAuthority(); //com.lay.provider
String query = uri.getQuery(); //name=张三&age=23&sex=男
String name = uri.getQueryParameter("name");
String age = uri.getQueryParameter("age");
String sex = uri.getQueryParameter("sex");
ContentValues values1 = new ContentValues();
values1.put("name",name);
values1.put("age",age);
values1.put("sex",sex);
database.insert(StudentTable.TAB_NAME,null,values1);
Log.e("TAG","主机名"+authority+",参数:"+query);
}
return ContentUris.withAppendedId(uri,id);
重点看,当ContentValues 为空时,解析Uri,通过uri的各类API,来解析出uri携带的参数,也就是需要插入数据库的参数,重新创建一个新的ContentValues 类,插入数据库。
2020-06-05 20:39:23.028 2992-3053/com.example.providerdemo E/TAG: 主机名com.lay.provider,参数:name=张三&age=23&sex=男
4、访问系统通讯录和消息
对于系统的通讯录和消息,有自己的Uri ,那么通过ContentResolver,就可以实现查询(别忘记需要加权限)。
(1)读取手机短信息
短信的Uri:content://sms
收件箱:content://sms/inbox
发件箱:content://sms/sent
草稿箱:content://sms/draft
//动态获取权限
if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS)
!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_SMS},SMS_CODE);
}
lv_message = findViewById(R.id.lv_message);
resolver = getContentResolver();
//查询短消息Uri
Uri uri = Uri.parse("content://sms");
//查询全部
Cursor cursor = resolver.query(uri, null, null, null, null);
msgList = new ArrayList<>();
while (cursor.moveToNext()){
String address = cursor.getString(cursor.getColumnIndex("address"));
String body = cursor.getString(cursor.getColumnIndex("body"));
Message message = new Message(address,body);
msgList.add(message);
}
//设置适配器
adapter = new ListViewAdapter(this,msgList);
lv_message.setAdapter(adapter);
(2)读取手机联系人
连续人和短消息不同的是,短消息是所有的内容都在一张表中,通过查询可以拿到所有的数据;
但是联系人的姓名以及其他信息(手机号等)是在不同的表中的,也就说不能通过一张表拿到全部信息;但是在姓名表中,除了可以拿到姓名之外,还可以拿到主键,那么该主键就是从表的外键,可以根据主键查询对应的数据。
通讯录Uri:ContactsContract.Contacts.CONTENT_URI
姓名:ContactsContract.Contacts.DISPLAY_NAME
主键:ContactsContract.Contacts._ID
电话号码Uri:ContactsContract.CommonDataKinds.Phone.CONTENT_URI
外键id:ContactsContract.CommonDataKinds.Phone.CONTACT_ID
电话号码:ContactsContract.CommonDataKinds.Phone.NUMBER
resolver = getContentResolver();
//获取姓名
Cursor cursor = resolver.query
(ContactsContract.Contacts.CONTENT_URI,
null, null, null, null);
contactList = new ArrayList<>();
while (cursor.moveToNext()){
//获取姓名
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
//主键
String _id = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
//查询电话号码
String selections = ContactsContract.CommonDataKinds.Phone.CONTACT_ID+"=?";
//根据从键的id查询电话号码
Cursor cursor1 = resolver.query
(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
selections,
new String[]{_id},
null);
while (cursor1.moveToNext()){
//拿到电话号码
String number = cursor1.getString(cursor1.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
Contact contact = new Contact(name,number);
contactList.add(contact);
}
}
//设置适配器
adapter = new ContactListViewAdapter(this,contactList);
lv_contract.setAdapter(adapter);
别忘记添加权限:
//获取读取联系人权限
if(ContextCompat.checkSelfPermission(this,Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED){
//没有获取权限就去拿
ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},CONTRACTS_CODE);
}
(3)添加手机联系人
之前是查询query联系操作,现在可以完成insert
操作。
ContentResolver resolver = getContentResolver();
//首先插入一条空数据,得到id
ContentValues values = new ContentValues();
Uri uri = resolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
long id = ContentUris.parseId(uri);
//插入联系人
values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME,"王春华");
//姓名与ID绑定
values.put(ContactsContract.Data.RAW_CONTACT_ID,id);
//姓名类型
values.put(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
//添加
resolver.insert(ContactsContract.Data.CONTENT_URI,values);
//插入电话号码
values.clear();
values.put(ContactsContract.CommonDataKinds.Phone.NUMBER,"13155555555");
//电话号码与ID绑定
values.put(ContactsContract.Data.RAW_CONTACT_ID,id);
values.put(ContactsContract.Data.MIMETYPE,
//电话号码的Item类型
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
//电话号码的类型(移动 座机。。。。)
values.put(ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
//添加
resolver.insert(ContactsContract.Data.CONTENT_URI,values);
对于插入操作,涉及到的常量非常多,关键的思想就是,因为手机号和姓名不在同一张表中,但是他们的id的对应的,所以首先需要拿到id;在拿到id之后,分别与该id绑定,将姓名和手机号插入到数据库中。