Android高级应用2----ContentProvider(访问手机短信和通讯录数据)
在上一节《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绑定,将姓名和手机号插入到数据库中。
更多相关文章
- 谈谈android数据存储方式
- 【Android Training - 04】保存数据 [ Lesson 3 - 保存数据到SQL
- Android JNI 使用的数据结构JNINativeMethod详解 ||建立Android
- Android之SQlite数据库
- android平台下基于ffmpeg的swscale模块实现对YUV和RGB数据进行转
- Android通过加载其他应用的Dex文件破解关键数据
- Android入门篇六:使用意图传递数据之返回结果
- Android之ListActivity:布局与数据绑定