Я тестировал это решение в течение нескольких дней, и, кажется, все в порядке, но я думаю, что мне нужно протестировать его гораздо больше. Если вы используете этот метод, проведите собственное тестирование и, прежде всего, ПОЖАЛУЙСТА, ДАЙТЕ МНЕ ЗНАТЬ, ЕСЛИ Я ПРОПУСТИТ НИЧЕГО, и не спешите понижать рейтинг. Спасибо!
- Я создал класс App, который расширяет приложение и реализует ActivityLifecycleCallbacks. В котором я впервые создаю класс ContactSyn c и активирую его каждый раз, когда приложение выходит на первый план
- В классе ContactSyn c я использую Kotlin
withContext(Dispatchers.IO)
, чтобы приостановить любой код для более простой поток - Я использую .get (), чтобы получить все контакты из firestore, связанные с текущим пользователем
- в .get () addOnSuccessListener, я добавляю все контакты в HashMap с нормализованным номером телефона в качестве ключа и имени + идентификатор firestore в качестве значений (с использованием внутреннего класса)
- При создании HashMap я также проверяю, нет ли дубликатов в firestore с номером телефона smae и, если это так, удалите их (с помощью пакета)
- я затем получить все контакты с android телефона. Сначала я сортирую их по NORMALIZED_NUMBER и DISPLAY_NAME (объясню позже)
- Сейчас я создаю
batchArray
с индексом и счетом, чтобы избежать превышения лимита 500 - Я начинаю сканирование через курсор контактов ,
- Сначала я получаю нормализованный номер, если он недоступен (ноль), я создаю его самостоятельно, используя созданную мной функцию (возможно, нулевое значение возвращается только для телефонных номеров не в правильном формате, не конечно)
- Затем я сравниваю нормализованное число с предыдущим значением курсора. Если то же самое я игнорирую, чтобы избежать дубликатов в firestore (помните, что курсор отсортирован по NORMALIZED_NUMBER)
- Затем я проверяю, есть ли нормализованное число в HashMap.
- Если в HashMap: я сравниваю имя в HashMap с именем курсора. если отличается, я прихожу к выводу, что имя было изменено , и я обновляю контакт пожарного хранилища в пакетном массиве (не забывайте увеличивать счетчик и, если он превышает 500, увеличить индекс). Затем я удаляю нормализованный номер из HashMap, чтобы избежать его удаления позже
- Если не в HashMap: я заключаю, что контакт новый , и добавляю его в firestore через пакет
- Я перебираю весь курсор до завершения.
- Когда курсор завершен, я закрываю его
- Все оставшиеся записи, найденные в HashMap, - это те, которые не были найдены в хранилище, поэтому удалены. Я повторяю и удаляю их, используя пакет
- syn c выполняется на стороне телефона
Теперь, так как для создания фактического syn c требуется доступ ко всем пользователям, я пользовательские функции firebase в узле. Я создаю 2 функции:
- функция, которая срабатывает при создании нового пользователя (подписано по телефону)
- функция, которая срабатывает при создании нового документа контакта.
Обе функции сравнивают пользователей с нормализованным числом в документе и, если они совпадают, записывают uid этого пользователя в поле "friend_uid" документа firestore.
Обратите внимание, что вы можете иметь ошибки в эти функции, если вы пытаетесь использовать их в бесплатном плане Firebase. Я предлагаю перейти на план Blaze и ограничить плату до пары долларов. При переходе на Blaze Google также дает вам бесплатные дополнения и позволяет избежать фактического платежа
. На этом синхронизация c завершена. Синхронизация c занимает всего пару секунд
Чтобы отобразить все контакты, которые являются пользователями приложения, запросить все контакты пользователя с "friend_uid", которые не равны нулю.
Некоторые дополнительные примечания:
- .get () будет извлекать все контакты каждый раз, когда выполняется синхронизация c. Это может быть много чтения, если пользователь имеет пару сотен контактов. Чтобы свести к минимуму, я использую
.get(Source.DEFAULT)
при запуске приложения и .get(Source.CACHE)
в другое время. Поскольку названия и номера этих документов изменяются только пользователем, я считаю, что в большинстве случаев это не будет проблемой (тестирование продолжается) - Чтобы максимально сократить процесс syn c, я запускаю его, только если какой-либо контакт изменил свою временную метку. Я сохраняю последнюю метку времени в SharedPreferences и сравниваю ее. Я обнаружил, что в основном он сохраняет syn c при быстром повторном открытии приложения.
- Я также сохраняю последнего пользователя, вошедшего в систему. При любом изменении пользователя я заново инициализирую контакты текущего пользователя
Некоторый исходный код (все еще тестируется, пожалуйста, дайте мне знать, если есть ошибки):
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
}