Android联系人数据的获取与分页查询功能封装

作者: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

分页与查询效果图如下:

源码都在上面工具类中了,希望对大家有所帮助。😅😅

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这是一个集成目前Android主流优秀第三方组件、优秀好用的自定义控件、实用工具类封装、 以及一些APP共通模块(比如:版本更新、意见反馈、引导界面等等)的开发包,帮助程序员 快速开发自己的APP 已集成第三方开源组件: 网络请求库android-async-http 图片异步加载库universal-image-loader Sqlite数据库操作ormlite-android 社会化分享ShareSDK+短信验证码 Zxing二维码库 百度地图定位SDK 谷歌依赖注入库RoboGuice WebService调用库ksoap2 XML解析XStream 动画开源库nineoldandroids 表单验证库android-validation-komensky 更多优秀开源库等待集成... 已封装工具类: HTTP网络通信工具类(ToolHTTP.java),get/post请求,支持多种接口回调 SOAP协议通信工具类(ToolSOAP.java),基于异步回调请求WebService接口 Sqlite数据库操作工具类(ToolDatabase.java),获取DAO、创建表、删除表等API 提示信息对话框工具类(ToolAlert.java),已集成泡泡、土司、对话框三种提示 文件操作工具类(ToolFile.java),assets/raw/xml/shrePerface/等文件读写API 地图定位工具类(ToolLocation.java),读取GPS状态、请求定位、获取经纬度等方法 社会化分享工具类(ToolShareSDK.java),各大开发平台分享API操作 短信验证码工具类(ToolMSM.java),移动/联通/电信三网发送手机短信验证码、异步回调验 证结果 字符串操作工具类(ToolString.java),生成UUID、非空非NULL逻辑判断、生成MD5等常用共 通方法 数据操作工具类(ToolData.java),自动递归获取表单数据封装成Map、本地数据分页共通方 法等 图片操作工具类(ToolPicture.java),生成二维码、验证码、灰度、合成、圆角、水印等操 作 读取本地资源工具类(ToolResource.java),反射本地资源文件API,避免依赖R文件,方便 jar形式发布 Android单位转换工具类(ToolUnit.java),sp/dp/px互转 自定义Toast工具类(ToolToast.java),自定义背景色、字体颜色、大小、边框等 Properties操作工具类(ToolProperties.java),读写Properties文件操作 网络操作工具类(ToolNetwork.java),获取网络信息、更改切换网络等相关操作 日期操作工具类(ToolDateTime.java),获取日期、日期加减、格式化日期、日期转换等操作 XML操作工具类(ToolXml.java),基于DOM/XMLPullPaser模式解析、生成XML操作 XMPP操作工具类(ToolXMPP.java),基于XMPP协议的相关API操作 适配字体工具类(ToolAutoFit.java),代码根据设备密度自动缩放View的字体大小 LOG相关工具类(ToolLog.java功能待续-->切入记录异常日志,并存储文件或上传至服务 器 已封装/收集自定义控件: 兼容低版本的SwitchButton 追加自定义属性Value的CheckBox/RadioButton/RadioGroup/SingleSpinner 圆角提示信息TipsView 圆角图片RoundImageView 自定义样式风格Progres

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值