作者:newki
转载地址:https://juejin.cn/post/7104172399376990222
现在很多应用为了拉新,直接就获取你手机通讯录,查看当前联系人是否是我们的用户,如果不是我们的用户,就邀请他注册我们的应用。
常规操作了,微信也这么干过。本文的重点是我们自己的应用如何获取联系人呢?这就涉及到跨进程交互。我们自己的App和系统的联系人App通信,读取联系人App的数据。
AIDL! 哟,会抢答了。😄😄(关于AIDL之前有讲过)
没错,AIDL 是可以获取跨进程 App 的数据的,但是联系人 App 没提供对应的服务啊,因为一般来说 AIDL 可以提供一些动态数据,类似联系人这种本地数据都是通过 ContentProvider(内容提供者)来提供的。它适应于一些db,file,xml 等持久化数据的提供。
联系人App通过 ContentProvider 定义了一些方法,增删改查的的操作,并通过指定的URI来操作它们。 💪💪
它定义的方法大致如下:
<-- 4个核心方法 -->
public Uri insert(Uri uri, ContentValues values)
// 外部进程向 ContentProvider 中添加数据
public int delete(Uri uri, String selection, String[] selectionArgs)
// 外部进程 删除 ContentProvider 中的数据
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
// 外部进程更新 ContentProvider 中的数据
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
// 外部应用 获取 ContentProvider 中的数据
<-- 2个其他方法 -->
public boolean onCreate()
// ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用
// 注:运行在ContentProvider进程的主线程,故不能做耗时操作
public String getType(Uri uri)
// 得到数据类型,即返回当前 Url 所代表数据的MIME类型
大致 ContentProvider 的实现Demo如下:
public class MyProvider extends ContentProvider {
private Context mContext;
DBHelper mDbHelper = null;
SQLiteDatabase db = null;
public static final String AUTOHORITY = "cn.scu.myprovider";
// 设置ContentProvider的唯一标识
public static final int User_Code = 1;
public static final int Job_Code = 2;
// UriMatcher类使用:在ContentProvider 中注册URI
private static final UriMatcher mMatcher;
static{
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 初始化
mMatcher.addURI(AUTOHORITY,"user", User_Code);
mMatcher.addURI(AUTOHORITY, "job", Job_Code);
}
// 以下是ContentProvider的6个方法
/**
* 初始化ContentProvider
*/
@Override
public boolean onCreate() {
mContext = getContext();
// 在ContentProvider创建时对数据库进行初始化
// 运行在主线程,故不能做耗时操作,此处仅作展示
mDbHelper = new DBHelper(getContext());
db = mDbHelper.getWritableDatabase();
// 初始化两个表的数据(先清空两个表,再各加入一个记录)
db.execSQL("delete from user");
db.execSQL("insert into user values(1,'Carson');");
db.execSQL("insert into user values(2,'Kobe');");
db.execSQL("delete from job");
db.execSQL("insert into job values(1,'Android');");
db.execSQL("insert into job values(2,'iOS');");
return true;
}
/**
* 添加数据
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
// 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
// 该方法在最下面
String table = getTableName(uri);
// 向该表添加数据
db.insert(table, null, values);
// 当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
mContext.getContentResolver().notifyChange(uri, null);
return uri;
}
/**
* 查询数据
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
// 该方法在最下面
String table = getTableName(uri);
// 查询数据
return db.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
}
/**
* 更新数据
*/
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
//...
return 0;
}
/**
* 删除数据
*/
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
//...
return 0;
}
@Override
public String getType(Uri uri) {
//...
return null;
}
/**
* 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
*/
private String getTableName(Uri uri){
String tableName = null;
switch (mMatcher.match(uri)) {
case User_Code:
tableName = DBHelper.USER_TABLE_NAME;
break;
case Job_Code:
tableName = DBHelper.JOB_TABLE_NAME;
break;
}
return tableName;
}
}
而我们通过 ContentResolver 对象可以访问到指定的定义好的 ContentProvider。
话不多说下面我们通过内容接受者 ContentResolver 获取联系人App在 ContentProvider 定义好的数据吧。
默认的方法是全部获取:(不推荐了)
/**
* 获取全部的联系人(基础方式,慢速方式)
*/
public static ArrayList<MyLocalContact> getAllContacts(Context context) {
ArrayList<MyLocalContact> contacts = new ArrayList<>();
Cursor cursor = context.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
while (cursor.moveToNext()) {
//新建一个联系人实例
MyLocalContact temp = new MyLocalContact();
String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
//获取联系人姓名
String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
temp.name = name;
//获取联系人电话号码
List<String> phoneList = new ArrayList<>();
Cursor phoneCursor = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=" + contactId, null, null);
while (phoneCursor.moveToNext()) {
String phone = phoneCursor.getString(phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
phone = phone.replace("-", "");
phone = phone.replace(" ", "");
phoneList.add(phone);
}
temp.phones = StringListUtils.list2CommaStr(phoneList);
contacts.add(temp);
//记得要把cursor给close掉
phoneCursor.close();
}
cursor.close();
return contacts;
}
经过测试如果联系人达到1000人以上就会很慢,因为是一次读取出来的,大概要30S以上。而通过上面的Demo我们可以知道 联系人App内部的 ContentProvider 实现也是基于 Sqlite 实现的数据操作,那我们是不是可以通过 getContentResolver 传递SQL的查询参数,从而实现分页功能与查询功能呢?
答案是肯定的!具体方法如下:
/**
* SQL方式查询(推荐)
* 分页数据
* 模糊匹配
*/
public static ArrayList<MyLocalContact> getContactsLimit(Context context, int pageSize, int page, String keyowrd) {
ArrayList<MyLocalContact> contacts = new ArrayList<>();
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
//指定要查询的数量
String[] projection = {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID};
if (page == 1) {
allPhoneCount = getAllPhoneCount(context);
}
int pages = allPhoneCount / pageSize + (allPhoneCount % pageSize == 0 ? 0 : 1);
if (page < 1) {
page = 1;
} else if (page > pages) {
page = pages;
}
int limit = pageSize <= 0 ? 30 : pageSize;
int currentOffset = (page - 1) * limit;
//指定的排序和分页规则,按照姓名排序
// String limitSql = ContactsContract.Contacts._ID + " ASC limit " + pageSize + " offset " + currentOffset;
String limitSql = ContactsContract.Contacts.DISPLAY_NAME + " ASC limit " + pageSize + " offset " + currentOffset;
YYLogUtils.w("limitSql:" + limitSql);
//搜索SQL,当有搜索条件的时候填入搜索的Sql
String searchSql;
if (!CheckUtil.isEmpty(keyowrd)) {
searchSql = ContactsContract.CommonDataKinds.Phone.NUMBER + " like " + "'%" + keyowrd + "%'" +
" or " +
ContactsContract.Contacts.DISPLAY_NAME + " like " + "'%" + keyowrd + "%'";
} else {
searchSql = null;
}
//构建查询
Cursor cursor = context.getContentResolver().query(uri, projection, searchSql, null, limitSql);
while (cursor.moveToNext()) {
//新建一个联系人实例
MyLocalContact temp = new MyLocalContact();
//获取联系人姓名
temp.name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
//获取联系人电话号码
String phone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
temp.phones = phone.replace(" ", "");
temp.phone = temp.phones;
contacts.add(temp);
}
cursor.close();
return contacts;
}
/**
* 获取系统联系人总数量
*/
public static int getAllPhoneCount(Context context) {
int num = 0;
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
String[] projection = {ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.DATA1,
ContactsContract.CommonDataKinds.Phone.CONTACT_ID};
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
if (null != cursor) {
num = cursor.getCount();
cursor.close();
}
return num;
}
MyLocation只是我自定义的一个数据对象
public class MyLocalContact implements Serializable {
public String name = "";
public String phones = "";
@SerializedName("other")
public long id;
public String member_id = "";
public String member_name = "";
public String nick_name = "";
public String country_code = "";
@SerializedName(value = "phone", alternate = "member_mobile")
public String phone = "";
@SerializedName(value = "avatar", alternate = "member_avatar")
public String avatar = "";
public boolean app_member = false;
}
使用的使用:
先定义和申请权限 Manifest.permission.READ_CONTACTS
。
具体调用如下:
private void getContacts(int curPage) {
List<MyLocalContact> allContacts = new ArrayList<>();
Observable.just(mKeyword)
.subscribeOn(Schedulers.io())
.map(keyword -> {
//异步获取本地联系人数据
ArrayList<MyLocalContact> localContacts = LocalContactUtils.getContactsLimit(mActivity, LIMIT_PAGE_SIZE, curPage, keyword);
allContacts.addAll(localContacts);
YYLogUtils.w("查询本地通讯录:" + allContacts.toString() + " Size:" + allContacts.size() + "Curpage:" + curPage);
return allContacts;
})
.flatMap((Function<List<MyLocalContact>, ObservableSource<BaseBean<List<MyLocalContact>>>>) myLocalContacts -> {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < myLocalContacts.size(); i++) {
MyLocalContact contact = myLocalContacts.get(i);
builder.append(contact.phone);
if (i < myLocalContacts.size() - 1) {
builder.append("|");
}
}
//请求网络匹配数据
return mGlobalModel.matchContacts(checkTokenAndStutus(), builder.toString());
})
.map((Function<BaseBean<List<MyLocalContact>>, List<MyLocalContact>>) listBaseBean -> {
if (listBaseBean.getCode() == 200) {
//转换匹配之后的数据
List<MyLocalContact> list = listBaseBean.getData();
if (!CheckUtil.isEmpty(list)) {
for (int i = 0; i < list.size(); i++) {
MyLocalContact remoteContact = list.get(i);
int j = allContacts.indexOf(remoteContact);
if (j < 0) {
remoteContact.name = remoteContact.member_name;
remoteContact.app_member = true;
allContacts.add(remoteContact);
continue;
}
MyLocalContact localContact = allContacts.get(j);
localContact.member_id = remoteContact.member_id;
localContact.nick_name = remoteContact.member_name;
localContact.avatar = remoteContact.avatar;
localContact.app_member = true;
}
}
}
return allContacts;
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new HandleErrorVMSubscriber<List<MyLocalContact>>() {
@Override
public void addDisposableToList(Disposable disposable) {
mDisposables.add(disposable);
}
@Override
public void onFailedMessage(String msg) {
loadError(msg);
YYLogUtils.e(msg);
mLoadLiveData.postValue(false);
}
@Override
public void onNext(@NonNull List<MyLocalContact> myLocalContacts) {
loadSuccess();
mLoadLiveData.postValue(true);
handleData(myLocalContacts);
}
});
}
注意这里涉及到具体的业务逻辑了,先获取到30个联系人,然后查询该联系人电话是否已经注册到我们的App了,如果已经注册了就改变数据的状态(为了在Adapter上展示邀请按钮)。数据转换完成之后再在列表上展示。所以实际上我们的RV列表也是做了上拉加载的Loading的。配合联系人数据库查询和远端服务器注册人员校验。
其实调用数据库就只有上面 getContactsLimit
方法而已,大家可以自行拿取重点方法 getContactsLimit
分页与查询效果图如下:
源码都在上面工具类中了,希望对大家有所帮助。😅😅
网友评论