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