美文网首页Android开发Android开发
Android联系人数据的获取与分页查询功能封装

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

作者: 丘卡皮 | 来源:发表于2022-06-02 14:09 被阅读0次

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

    分页与查询效果图如下:

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

    相关文章

      网友评论

        本文标题:Android联系人数据的获取与分页查询功能封装

        本文链接:https://www.haomeiwen.com/subject/kajqmrtx.html