安卓组件之内容提供者
1、什么是内容提供者
内容提供者,我们从字面上认识,就是一个提供内容的东西!
这东西常用吗?用得比较少,有一个项目用上了,就是蓝牙电话。为什么会用上呢?因为需要拿到联系人的数据!
内容提供者,就是向第三方暴露自己的数据库的!目前来说,只有google的短信/电话是这么做的,其他应用基本上不会这么做!所以呢,使用场景也少了!比如说微信/支付宝,在第一次使用的时候会询问你是否同意让它读取你的联系人/通讯录,就是通过内容提供者来读取的。
2、为什么要使用内容提供者呢?
我们可以看出,数据库的权限是:
熟悉Linux的同学都知道,Linux的权限是这样分的,(-rw)前面个是用户的权限,(-rw)中间三个是同一个用户组的权限,(—)后面三个是其他用户的权限。
在android里面,每一个应用可以看做是一个应用,那么我们可以认为,其他应用是没办法访问到这个数据库的。有些情况下,需要访问别人的数据库,这个时候就需要我们的内容提供者了!
比如说今日头条需要访问淘宝的内容,根据淘宝的用户访问习惯来给使用今日头条的用户推荐定向广告,比如说我们开发一个蓝牙电话,需要向手机的联系人这个应用拿到手机的所有联系人,etc.
内容提供者,就是给别人暴露我们本应用里的数据库的,至于想暴露那些数据库,怎么样的其他应用才能访问,就看后面的内容吧!
3、内容提供者
创建内容提者的步骤:
第一步:编写一个类 继承自内容提供者(ContentProvider)这个例子的目的是让大家知道这个内容提供者的工作流程就够了!实际的使用我们后面再详细说明吧!
四大组件都是要继承自XXX的,总结到了吗?为什么呢?因为它要由系统去创建,生命周期由系统去管理呀!
package com.sunofbeaches.providerdemo.provider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Created by TrillGates on 18/7/15.
* God bless my code!
*/
public class StudentScoreProvider 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;
}
}
第二步:注册一下呗!
我们四大组件都要注册,这个知道吧!所以我们需要在AndroidManifest.xml里进行注册!
我们可以发现,有两个参数必填的哦!name我们知道,是这填写空上类的全路径名称。
authorities是什么呢?其实就是令牌,口令,暗号!对得上,匹配得了的,才有权限来操作数据库。
一般来说,这个我们填写报名就OK了!如下:
provider最好加多一项:
android:exported=”true”
我们内容提供者的代码怎么写呢?
我们先暂时写一个查询的代码:
package com.sunofbeaches.providerdemo.provider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import com.sunofbeaches.providerdemo.db.StudentScoreDBHelper;
/**
* Created by TrillGates on 18/7/15.
* God bless my code!
*/
public class StudentScoreProvider extends ContentProvider {
StudentScoreDBHelper mStudentScoreDBHelper;
//定义一个Uri匹配器,参数表示不匹配的时候返回什么值,这里返回的是-1,也就是说-1表示不匹配
private static UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//这个表示匹配
private static final int CODE_MATCH = 1;
static {
//添加匹配规则,前面是authority,这个其实就是我们在配置文件里配置的那个认证字符串
//第二个参数是path,一般表示表名(可以为*或者#,表示所有的字母或者数字)
//第三个表示the code that is returned when a URI is matched,也就是说规则匹配则会返回后面那个code
// 否则返回前面我们指定的默认 UriMatcher.NO_MATCH
mUriMatcher.addURI("com.sunofbeaches.providerdemo","sob_score",CODE_MATCH);
}
@Override
public boolean onCreate() {
//注意,这里getContext()
//Only available once
// * {@link #onCreate} has been called
//里面的注释是:只有当onCreate方法被调用以后,getContext这个方法才可用。
mStudentScoreDBHelper = new StudentScoreDBHelper(getContext());
return false;
}
@Override
public Cursor query(Uri uri, String[] strings, String s, String[] strings1, String s1) {
int match = mUriMatcher.match(uri);
if (match==CODE_MATCH) {
SQLiteDatabase readableDatabase = mStudentScoreDBHelper.getReadableDatabase();
return readableDatabase.query(StudentScoreDBHelper.TABLE_NAME, strings, s, strings1, s1, null, null);
}else{
throw new IllegalArgumentException("Uri not matching.");
}
}
@Override
public String getType( Uri uri) {
return null;
}
@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;
}
}
相关的细节已经在注释里了!
接下来,我们要写另外一个应用,通过内容提供者的方式来获取到当前应用的数据了。
package com.sunofbeaches.providerdemoteacher;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void getScore(View view) {
ContentResolver contentResolver = this.getContentResolver();
Uri uri = Uri.parse("content://com.sunofbeaches.providerdemo/sob_score");
Cursor cursor = contentResolver.query(uri, null, null, null, null);
while (cursor.moveToNext()) {
Log.d(TAG, "id is -- > " + cursor.getInt(0));
Log.d(TAG, "name is -- > " + cursor.getString(1));
Log.d(TAG, "Chinese is -- > " + cursor.getInt(2));
Log.d(TAG, "Math is -- > " + cursor.getInt(3));
Log.d(TAG, "English is -- > " + cursor.getInt(4));
}
cursor.close();
}
}
执行结果怎么样的呢?
OK,到这里的话,我们成功地读取到了另外一个应用的数据库内容了。
我们稍微总结一下:
首先是有一个数据库,但是这个数据库是别人家的呀,但是这个别人家的数据库有一个内容提供者呢!
我们只要知道对应的认证就可以读取到数据了!
所以,接下来我们就要仿今日头条,仿腾讯QQ,微信这些应用获取到手机号码,也就是通讯录的内容。
4、内容观察者
https://blog.csdn.net/qq_39826207/article/details/128204996
5、ContentProvider(内容提供者)的应用
动态获取权限(示例)
private void checkPermission() {
//先判断有没有权限
int i = checkSelfPermission("android.permission.READ_EXTERNAL_STORAGE");
if(i != PackageManager.PERMISSION_GRANTED){
//没有权限
// 申请权限
// 定义需要申请的权限数组
String[] permission = new String[] {
Manifest.permission.READ_EXTERNAL_STORAGE
};
// 定义这次申请权限的唯一请求编码
int requestCode = 2023;
// 申请权限
requestPermissions(permission, requestCode);
}
}
/**
* 申请权限的回调方法
* @param requestCode 申请权限时的请求码
* @param permissions 所有权限的数组
* @param grantResults 返回获取权限的状态码数组(-1:获取失败----0:获取成功)
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == 2023){
Log.d(TAG, "grantResults: "+grantResults[0]);
Log.d(TAG, "grantResults: "+grantResults.length);
if(grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
//有权限
Toast.makeText(this, "已经获得所有权限!", Toast.LENGTH_SHORT).show();
}else {
//没有权限
//根据交互数据
Toast.makeText(this, "没有获得权限!,程序将退出!", Toast.LENGTH_SHORT).show();
}
}
}
前面代码中我有所说道,一般情况下我们不会自定义内容提供者,谁会白白的访问一个没有任何用途的app去操纵数据库中的数据,相反我们往往会使用系统提供的内容提供者,比如说日历联系人短信等对这些数据库中的表进行数据的操作。通过前面使用自定义的内容提供者,接下里我们可以使用系统内容提供者,使用步骤相似。
在下面使用系统提供的内容提供者,操作步骤无非就是看上层应用源码,Manifest中找authorities,Provider类中找url和path,注册权限,对表进行操作 !因为下面的案例都需要动态获取权限,我没有贴相应权限的代码,关于权限的动态获取,请转到Android6.0+应用权限
案例一 获取日历事件,向日历中添加提醒事件
日历内容提供者的相关信息,都可以以从android开发平台查到。
常用表:红线标出
- Calendars表
ContentResolver contentResolver = getContentResolver();
// 1.获取日历相关的Uri,content://authorities/path,从android日历源码Manifest中查找authorities
//Uri uri = Uri.parse("content://" + CalendarContract.AUTHORITY + "/calendars");
Uri uri = CalendarContract.Calendars.CONTENT_URI;
Cursor cursor = contentResolver.query(uri,null,null,null,null);
String[] columnNames = cursor.getColumnNames();
while(cursor.moveToNext()) {
Log.d(TAG,"=============================================");
for(String columnName : columnNames) {
Log.d(TAG,"field --- > " + columnName + "value -- > " + cursor.getString(cursor.getColumnIndex(columnName)));
}
Log.d(TAG,"=============================================");
}
cursor.close();
查询到的都是日历表相关的信息,比如日历所属id,日历账户名称…
- Events表
获取日历中相关事件,这里我们查询所有,只需替换上文Calendars代码中uri
Uri uri = CalendarContract.Events.CONTENT_URI; 查询事件结果如下:
D/MainActivity: =============================================
D/MainActivity: field --- > originalAllDay; value -- > null
D/MainActivity: field --- > account_type; value -- > com.android.huawei
D/MainActivity: field --- > exrule; value -- > null
D/MainActivity: field --- > mutators; value -- > com.android.calendar
D/MainActivity: field --- > originalInstanceTime; value -- > null
D/MainActivity: field --- > allDay; value -- > 0
D/MainActivity: field --- > allowedReminders; value -- > 0,1
D/MainActivity: field --- > rrule; value -- > null
D/MainActivity: field --- > location_longitude; value -- > c37d19942d3d
D/MainActivity: field --- > canOrganizerRespond; value -- > 1
D/MainActivity: field --- > lastDate; value -- > 1582896600000
D/MainActivity: field --- > visible; value -- > 1
D/MainActivity: field --- > calendar_id; value -- > 1
D/MainActivity: field --- > hasExtendedProperties; value -- > 0
D/MainActivity: field --- > calendar_access_level; value -- > 700
D/MainActivity: field --- > selfAttendeeStatus; value -- > 0
D/MainActivity: field --- > version; value -- > 2
D/MainActivity: field --- > allowedAvailability; value -- > 0,1
D/MainActivity: field --- > eventColor_index; value -- > null
D/MainActivity: field --- > isOrganizer; value -- > 1
D/MainActivity: field --- > _sync_id; value -- > null
D/MainActivity: field --- > event_image_type; value -- > null
D/MainActivity: field --- > calendar_color_index; value -- > null
D/MainActivity: field --- > _id; value -- > 199
D/MainActivity: field --- > guestsCanInviteOthers; value -- > 1
D/MainActivity: field --- > allowedAttendeeTypes; value -- > 0,1,2
D/MainActivity: field --- > dtstart; value -- > 1582893000000
D/MainActivity: field --- > guestsCanSeeGuests; value -- > 1
D/MainActivity: field --- > eventTimezone; value -- > Asia/Shanghai
D/MainActivity: field --- > availability; value -- > 0
D/MainActivity: field --- > title; value -- > 创新创业项目
D/MainActivity: field --- > ownerAccount; value -- > Phone
D/MainActivity: field --- > duration; value -- > null
D/MainActivity: field --- > event_calendar_type; value -- > 0
D/MainActivity: field --- > lastSynced; value -- > 0
D/MainActivity: field --- > guestsCanModify; value -- > 0
D/MainActivity: field --- > rdate; value -- > null
D/MainActivity: field --- > maxReminders; value -- > 5
D/MainActivity: field --- > account_name; value -- > Phone
D/MainActivity: field --- > calendar_color; value -- > -16733458
D/MainActivity: field --- > cal_sync9; value -- > null
D/MainActivity: field --- > cal_sync8; value -- > null
D/MainActivity: field --- > dirty; value -- > 1
D/MainActivity: field --- > calendar_timezone; value -- > Asia/Shanghai
D/MainActivity: field --- > accessLevel; value -- > 0
D/MainActivity: field --- > eventLocation; value -- > null
D/MainActivity: field --- > location_latitude; value -- > c37d19942d3df
D/MainActivity: field --- > hasAlarm; value -- > 1
D/MainActivity: field --- > uid2445; value -- > null
D/MainActivity: field --- > deleted; value -- > 0
D/MainActivity: field --- > eventColor; value -- > null
D/MainActivity: field --- > organizer; value -- > Phone
D/MainActivity: field --- > eventStatus; value -- > 1
D/MainActivity: field --- > customAppUri; value -- > null
D/MainActivity: field --- > canModifyTimeZone; value -- > 1
D/MainActivity: field --- > eventEndTimezone; value -- > null
D/MainActivity: field --- > customAppPackage; value -- > null
D/MainActivity: field --- > original_sync_id; value -- > null
D/MainActivity: field --- > hasAttendeeData; value -- > 1
D/MainActivity: field --- > displayColor; value -- > -16733458
D/MainActivity: field --- > dtend; value -- > 1582896600000
D/MainActivity: field --- > original_id; value -- > null
D/MainActivity: field --- > sync_data10; value -- > null
D/MainActivity: field --- > calendar_displayName; value -- > Phone
D/MainActivity: =============================================
向日历中插入提醒事件,这里结合Reminders表进行插入带有提醒的事件
- Reminders表
插入提醒事件,官方说明
/**
* 插入新事件时,必须包含以下字段:
* dtstart
* dtend如果事件不是重复发生
* 事件持续发生的持续时间
* rrule或rdate(如果事件重复发生)
* eventTimezone
* 一个calendar_id
* @param view
*/
public void addNewEvents(View view) {
ContentResolver contentResolver = getContentResolver();
Uri uri = CalendarContract.Events.CONTENT_URI;
ContentValues values = new ContentValues();
putValues(values);
Uri insert = contentResolver.insert(uri, values);
String eventId = insert.getLastPathSegment();
Log.d(TAG, "event id -->" + eventId);
addEventAlert(eventId);
Log.d(TAG, "insert result --->" + insert);
}
private void putValues(ContentValues values) {
// 日历ID 必填项,通过查询Events表,在log—cat下查看calendar_idvalue=1
long calID = 1;
// java.util下的Calendar,月份基准为0
Calendar beginTime = Calendar.getInstance();
// 2020 4-5 12:00
beginTime.set(2020, 3, 5,12,00);
Calendar endTime = Calendar.getInstance();
endTime.set(2020, 3, 5, 13, 00);
long startMillis = beginTime.getTimeInMillis();
long endMills = endTime.getTimeInMillis();
values.put(CalendarContract.Events.CALENDAR_ID, 1);
// 开始时间
values.put(CalendarContract.Events.DTSTART, startMillis);
// 结束时间
values.put(CalendarContract.Events.DTEND, endMills);
// 标题
values.put(CalendarContract.Events.TITLE, "QQ回复");
// 描述
values.put(CalendarContract.Events.DESCRIPTION, "QQ收到消息 ");
// 时间时区
String timeZone = TimeZone.getDefault().getID();
Log.d(TAG, "EventTimeZone-->"+ timeZone);
values.put(CalendarContract.Events.EVENT_TIMEZONE, timeZone);
}
案例二 获取联系人相应信息
在使用之前,我先在模拟器中新建了几个联系人,然后导出databases文件查看contacts2.db其中的表结构,然后在进行分析!
raw_contacts:包含联系人数据摘要的行,针对特定用户帐户和类型。 mimetypes:数据类型描述表 data:数据表,存储号码,QQ,邮箱,昵称,图片。这些类型都在mimetypes里有。 所以我们要拿一个用户的数据,应该是从raw_contacts里拿到id,再到data拿出数据,根据mimetype来判断数据的类型。 但是现在的android系统不这么玩了。也不直接给你读取mimetypes里的数据了。
但我们可以通过下面的uri获取 content://com.android.contacts/data/phones联系人信息
ContentResolver contentResolver = getContentResolver();
Uri contentUri = Uri.parse("content://com.android.contacts/raw_contacts");
Cursor cursor = contentResolver.query(contentUri, null, null, null, null);
List<Contact> contacts = new ArrayList<>();
Contact contact = null;
while (cursor.moveToNext()) {
contact = new Contact();
String id = cursor.getString(cursor.getColumnIndex("_id"));
contact.setId(id);
contact.setDisplayName(cursor.getString(cursor.getColumnIndex("display_name")));
Log.d(TAG,"id -- > " + id);
contacts.add(contact);
}
cursor.close();
Uri phoneNumUri = Uri.parse("content://com.android.contacts/data/phones");
for(Contact contact1 : contacts) {
Cursor phoneCursor = contentResolver.query(phoneNumUri,new String[]{"data1"},"raw_contact_id=?",new String[]{contact1.getId()},null);
if(phoneCursor.moveToNext()) {
contact1.setPhoneNum(phoneCursor.getString(phoneCursor.getColumnIndex("data1")).replace("-",""));
}
phoneCursor.close();
}
for(Contact contact1 : contacts) {
Log.d(TAG,contact1.getId() + " -- NAME --- > " + contact1.getDisplayName() + " --- PHONE -- > " + contact1.getPhoneNum());
}
Contact.java
package cn.wjx.contentprovider_test.model;
/**
* @author WuChangJian
* @date 2020/4/5 13:08
*/
public class Contact {
//ID
private String id;
//手机号
private String phoneNum;
//显示名称
private String displayName;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPhoneNum() {
return phoneNum;
}
public void setPhoneNum(String phoneNum) {
this.phoneNum = phoneNum;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
}
案例三 读取短信验证码
前面自定义内容提供者的时候,我们通过注册ContentObserver,当我们向表中插入一条数据时,可以监听到数据库表的变化,并且可以获取插入后的Uri(table/id);这就是通过注册内容观察者来实现的,接下来我们通过ContentObserver来监听SMS表中数据的变化,实现短信验证码的动态填充功能。 1. 我们想要了解短信相应表中有哪些字段为我们所用,最好的办法就是查询
private void getSMS() {
ContentResolver resolver = getContentResolver();
Uri uri = Uri.parse("content://sms");
Cursor cursor = resolver.query(uri, null, null, null, null);
String[] columnNames = cursor.getColumnNames();
while(cursor.moveToNext()) {
Log.d(TAG,"=============================================");
for(String columnName : columnNames) {
Log.d(TAG,"field --- > " + columnName + "; value -- > " + cursor.getString(cursor.getColumnIndex(columnName)));
}
Log.d(TAG,"=============================================");
}
cursor.close();
}
从下图可以看到,短信的内容被一个body所索引,body字段是我们需要的一个字段
2. 注册内容观察者ContentObserver,实现对接受短信的监听
注意点,当observer监听到短信的变化之后,onChange会执行两次,第一次不被所需。
3. 编码实现额外功能
倒计时的实现
// 利用CountDownTimer实现按钮的倒计时 private CountDownTimer mCountDownTimer = new CountDownTimer(总时长毫秒, interval) { @Override public void onTick(long millisUntilFinished) { // 防止用户重复点击,可以将按钮置为禁用等状态 … }
@Override
public void onFinish() {
// 倒计时结束后可做,按钮恢复状态
...
}
};
```
* 正则表达式匹配短信内容的数字
```
// 这里以六位验证码为例
Pattern p = Pattern.compile("(?<![0-9])([0-9]{6})(?![0-9])");
Matcher matcher = p.matcher(body);
boolean contain = matcher.find();
if (contain) {
Log.d(TAG, "VerifyCode --> "+matcher.group());
// 将验证码设置到UI控件上
...
}
```
6、用LoaderManger来获取媒体库图片数据
示例:获取本地图片相关参数的写法
private void initLoaderManger() {
LoaderManager loaderManager = LoaderManager.getInstance(this);
loaderManager.initLoader(1, null, new LoaderManager.LoaderCallbacks<Cursor>() {
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
if(id == 1){
return new CursorLoader(OneActivity.this,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[]{"_data","_display_name","date_added"},
null,null,"date_added DESC");
}
return null;
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
if(cursor != null){
String[] columnNames = cursor.getColumnNames();
while (cursor.moveToNext()){
Log.d(TAG, "==================================");
for (String columnName : columnNames){
//可以正常的获取到想要的字段
Log.d(TAG,"field --- > " + columnName + "value -- > " + cursor.getString(cursor.getColumnIndex(columnName)));
}
Log.d(TAG, "==================================");
}
cursor.close();
}
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
}
});
}