如何检测更新的android联系人并同步到firestore

Posted

技术标签:

【中文标题】如何检测更新的android联系人并同步到firestore【英文标题】:How to detect android contact updated and sync to firestore 【发布时间】:2020-05-11 10:33:34 【问题描述】:

我正在尝试获取所有已更新的 android 联系人。

我正在 Firebase 上保存我添加的最后一个联系人 ID 和最后更新的时间戳

我正在使用下一个函数来取回所有更新联系人的光标以与 firebase 服务器进行比较

private fun getUpdatedContacts(): Cursor? 

    val projection = arrayOf(
            ContactsContract.Contacts._ID,
            ContactsContract.Contacts.DISPLAY_NAME,
            ContactsContract.Contacts.HAS_PHONE_NUMBER,
            ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)

    val selection = ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ? AND " +
            ContactsContract.Contacts._ID + "<= ?"

    val selectionArgs = arrayOf(mFireContactDetails!!.lcu_ms.toString(), mFireContactDetails!!.lcid.toString())

    val sortOrder = ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " ASC"

    return mContentResolver.query(
            ContactsContract.Contacts.CONTENT_URI,
            projection,
            selection,
            selectionArgs,
            sortOrder)

但是当我更改手机中的一个联系人时,此光标会返回许多我从未使用过的不相关联系人并将它们标记为已更改。上次当我刚刚将电话号码添加到现有联系人时,我从该光标返回超过 50 个已更新的联系人。

Android 上发生了什么?我现在正在尝试同步过去 3 个月的联系人。为什么这么难???

【问题讨论】:

【参考方案1】:

这与您的其他问题的答案几乎相同:When deleting a contact on android, other random contacts id's being changed

您对联系人 ID 有一些您无法做出的假设 - 没有人保证联系人 ID 是递增的,也没有人保证联系人 ID 是稳定的,事实上它们绝对不是。

您可以在应用程序运行时使用查询的联系人 ID,它们在几分钟内被更改的可能性很小,但有可能每隔一段时间就更改现有用户的 ID。 不仅如此,同一个ID今天可以指向某个联系人,明天又可以指向完全不同的联系人。

如果您在云中保留了一些本地联系人的克隆,您应该使用以下复合 ID 来引用联系人: Contacts.CONTACT_ID、Contacts.LOOKUP_KEY、Contacts.DISPLAY_NAME

请在此处查看我的答案以获取更多详细信息:How to uniquely identify a contact on ContactsContract.Contacts table

这不是一个完美的解决方案,但它是我们拥有的最好的解决方案

【讨论】:

谢谢你的回答,我真的很挣扎。这是相同的代码,但不同的问题。我也看到了您的答案,但没有看到它如何帮助您找到有效的解决方案。如果 CONTACT ID 标有新的 TIME_STAMP,假设任何事情都可以更改,同步联系人的步骤是什么?与小的更改相比,我尝试的任何解决方案都涉及到对 firebase 的大量编码和高速率读/写。显然,对于这样一个常见的需求,必须有一个优雅的解决方案。我错了吗?? 不要在你的firebase克隆上使用android的contact-ids,而是为你的联系人创建一个新的随机id,并在你的android应用上创建一个从本地contact-id到云的映射- ID。如果您检测到某个联系人已更改其 ID(例如,意外名称或查找键),请使用 lookupUri 方法查找新的联系人 ID 以表示该联系人(它可能仍然是同一个),然后仅更新映射 嗨,marmor,我使用了您对 HashMap 的建议,但进行了修改。如果您有时间,请查看它。提前谢谢你。【参考方案2】:

我已经测试这个解决方案几天了,看起来还可以,但我认为我需要进行更多测试。如果您使用这种方法,请进行自己的测试,最重要的是,如果我遗漏了任何东西,请告诉我,不要急于降级。谢谢!

    我构建了一个 App 类,它扩展了 Application 并实现了 活动生命周期回调。我在其中创建了一个 ContactSync 类 第一次并在每次应用程序进入前台时激活它 在 ContactSync 类中,我正在使用 Kotlin withContext(Dispatchers.IO) 暂停任何代码以便于流程 我使用 .get() 从 firestore 中获取与当前用户相关的所有联系人 在 .get() addOnSuccessListener 中,我将所有联系人添加到 HashMap,其中标准化的电话号码作为键,名称 + firestore id 作为值(使用内部类) 在制作 HashMap 时,我还要确保 Firestore 上没有带有 smae 电话号码的重复项,如果有,请删除它们(使用批处理) 然后我从安卓手机中检索所有联系人。我先按 NORMALIZED_NUMBER 和 DISPLAY_NAME 对它们进行排序(稍后会解释) 我现在正在创建一个带有索引和计数的 batchArray,以避免超过 500 个限制 我开始扫描联系人光标, 我首先获取标准化号码,如果不可用(null),我会使用我自己创建的函数创建它(可能只为格式不正确的电话号码返回空值,不确定)李> 然后我将标准化数字与之前的光标值进行比较。如果相同,我会忽略它以避免在 firestore 中重复(请记住光标按 NORMALIZED_NUMBER 排序) 然后我检查规范化的数字是否已经在 HashMap 中。 如果在 HashMap 中:我将 HashMap 中的名称与游标名称进行比较。如果不同,我断定名称已更改,并在批处理数组中更新 firestore 联系人(记得增加计数器,如果超过 500 增加索引)。然后我从 HashMap 中删除规范化的数字以避免以后删除它 如果不在 HashMap 中:我断定联系人是新联系人,我通过批处理将其添加到 Firestore 我遍历所有光标直到完成。 当光标完成后我关闭它 在 HashMap 中找到的任何剩余记录都是在 firestore 上找不到的记录,因此被删除。我使用批处理迭代并删除它们 同步在手机端完成

现在,由于进行实际同步需要访问所有用户,我在 node.js 中使用 firebase 函数。我创建了 2 个函数:

    创建新用户时触发的函数(通过电话签名) 创建新联系人文档时触发的函数。

这两个函数将用户与文档中的规范化数字进行比较,如果匹配,则将该用户的 uid 写入 firestore 文档的“friend_uid”字段。

请注意,如果您尝试在免费的 Firebase 计划中使用这些功能,您可能会遇到错误。我建议更改为 Blaze 计划并将收费限制在几美元。通过更改为 Blaze,Google 还为您提供免费附加服务并避免实际付款

至此,同步完成。同步只需几秒钟

要显示应用程序的所有用户联系人,查询所有“friend_uid”不为空的用户联系人。

一些额外说明:

    .get() 将在每次进行同步时检索所有联系人。如果用户有数百个联系人,那可能会读很多。为了最小化,我在启动应用程序时使用.get(Source.DEFAULT),在其他时间使用.get(Source.CACHE)。由于这些文件的名称和编号仅由用户修改,我相信大多数时候不会有问题(仍在测试中) 为了尽可能减少同步过程,我仅在任何联系人更改其时间戳时才启动它。我将最后一个时间戳保存到 SharedPreferences 并进行比较。我发现它主要是在应用快速重新打开时保存同步。 我还保存了上次登录的用户。如果用户有任何变化,我会重新初始化当前用户联系人

部分源码(还在测试中,如有错误请告知):

private fun getContacts(): Cursor? 
    val projection = arrayOf(
            ContactsContract.CommonDataKinds.Phone._ID,
            ContactsContract.CommonDataKinds.Phone.NUMBER,
            ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER,
            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
            ContactsContract.CommonDataKinds.Phone.CONTACT_LAST_UPDATED_TIMESTAMP)

    //sort by NORMALIZED_NUMBER to detect duplicates and then by name to keep order and avoiding name change
    val sortOrder = ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER + " ASC, " +
            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME + " ASC"

    return mContentResolver.query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            projection,
            null,
            null,
            sortOrder)


    private suspend fun syncContactsAsync() = withContext(Dispatchers.IO)  

    if (isAnythingChanged() || mFirstRun) 

        if (getValues() == Result.SUCCESS) 
            myPrintln("values retrieved success")
         else 
            myPrintln("values retrieved failed. Aborting.")
            return@withContext
        

        val cursor: Cursor? = getContacts()

        if (cursor == null) 
            myPrintln("cursor cannot be null")
            mFireContactHashMap.clear()
            return@withContext
        

        if (cursor.count == 0) 
            cursor.close()
            mFireContactHashMap.clear()
            myPrintln("cursor empty")
            return@withContext
        

        var contactName: String?
        var internalContact: InternalContact?
        val batchArray = mutableListOf(FirebaseFirestore.getInstance().batch())
        var batchIndex = 0
        var batchCount = 0
        var normalizedNumber:String?
        var prevNumber = ""
        var firestoreId: String

        while (cursor.moveToNext()) 

            normalizedNumber = cursor.getString(COLUMN_UPDATED_NORMALIZED_NUMBER)

            if (normalizedNumber == null) 
                normalizedNumber = cursor.getString(COLUMN_UPDATED_PHONE_NUMBER)
                normalizedNumber = Phone.getParsedPhoneNumber(mDeviceCountryIso,normalizedNumber,mContext)
            

            //cursor sorted by normalized numbers so if same as previous, do not check
            if (normalizedNumber != prevNumber) 

                prevNumber = normalizedNumber

                contactName = cursor.getString(COLUMN_UPDATED_DISPLAY_NAME)
                internalContact = mFireContactHashMap[normalizedNumber]

                //if phone number exists on firestore
                if (internalContact != null) 

                    //if name changed, update in firestore
                    if (internalContact.name != contactName) 
                        myPrintln("updating $normalizedNumber from name: $internalContact.name to: $contactName")
                        batchArray[batchIndex].update(
                                mFireContactRef.document(internalContact.id),
                                FireContact.COLUMN_NAME,
                                contactName)

                        batchCount++
                    

                    //remove to avoid deletions
                    mFireContactHashMap.remove(normalizedNumber)
                 else 
                    //New item. Insert
                    if (normalizedNumber != mUserPhoneNumber) 
                        myPrintln("adding $normalizedNumber / $contactName")
                        firestoreId = mFireContactRef.document().id

                        batchArray[batchIndex].set(mFireContactRef.document(firestoreId),
                                FireContact(firestoreId, -1, contactName,
                                        cursor.getString(COLUMN_UPDATED_PHONE_NUMBER),
                                        normalizedNumber))
                        batchCount++
                    
                

                if (BATCH_HALF_MAX < batchCount ) 

                    batchArray += FirebaseFirestore.getInstance().batch()
                    batchCount = 0
                    batchIndex++
                
            
        

        cursor.close()

        //Remaining contacts not found on cursor so assumed deleted. Delete from firestore
        mFireContactHashMap.forEach  (key, value) ->
            myPrintln("deleting $value.name / $key")
            batchArray[batchIndex].delete(mFireContactRef.document(value.id))
            batchCount++
            if (BATCH_HALF_MAX < batchCount ) 
                batchArray += FirebaseFirestore.getInstance().batch()
                batchCount = 0
                batchIndex++
            
        

        //execute all batches

        if ((batchCount > 0) || (batchIndex > 0)) 
            myPrintln("committing changes...")
            batchArray.forEach  batch ->
                batch.commit()
            
         else 
            myPrintln("no records to commit")
        

        myPrintln("end sync")


        mFireContactHashMap.clear()
        mPreferenceManager.edit().putLong(PREF_LAST_TIMESTAMP,mLastContactUpdated).apply()

        mFirstRun = false
     else 
        myPrintln("no change in contacts")
    


private suspend fun putAllUserContactsToHashMap() : Result 

    var result = Result.FAILED

    val batchArray = mutableListOf(FirebaseFirestore.getInstance().batch())
    var batchIndex = 0
    var batchCount = 0

    mFireContactHashMap.clear()

    var source = Source.CACHE

    if (mFirstRun) 
        source = Source.DEFAULT
        myPrintln("get contacts via Source.DEFAULT")
     else 
        myPrintln("get contacts via Source.CACHE")
    


    mFireContactRef.whereEqualTo( FireContact.COLUMN_USER_ID,mUid ).get(source)
    .addOnSuccessListener documents ->

        var fireContact : FireContact

        for (doc in documents) 

            fireContact = doc.toObject(FireContact::class.java)

            if (!mFireContactHashMap.containsKey(fireContact.paPho)) 
                mFireContactHashMap[fireContact.paPho] = InternalContact(fireContact.na, doc.id)
             else 
                myPrintln("duplicate will be removed from firestore: $fireContact.paPho / $fireContact.na / $doc.id")

                batchArray[batchIndex].delete(mFireContactRef.document(doc.id))

                batchCount++

                if (BATCH_HALF_MAX < batchCount) 
                    batchArray += FirebaseFirestore.getInstance().batch()
                    batchCount = 0
                    batchIndex++
                
            
        

        result = Result.SUCCESS
    .addOnFailureListener  exception ->
        myPrintln("Error getting documents: $exception")
    .await()

    //execute all batches
    if ((batchCount > 0) || (batchIndex > 0)) 
        myPrintln("committing duplicate delete... ")
        batchArray.forEach  batch ->
            batch.commit()
        
     else 
        myPrintln("no duplicates to delete")
    

    return result

【讨论】:

以上是关于如何检测更新的android联系人并同步到firestore的主要内容,如果未能解决你的问题,请参考以下文章

如何检测是不是已在android中访问过联系人

ADB 无法在 Windows 7 上检测到我的 Kindle Fire,但设备管理器可以

Android:测试同步适配器

如何使用谷歌联系人api以编程方式将手机联系人同步到android中的gmail

检测应用程序正在 Kindle Fire 上运行

Android:仅获取更新和删除的联系人