2022-04-11 23:59:17 +00:00
|
|
|
package org.thoughtcrime.securesms.contacts.sync
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
import androidx.annotation.WorkerThread
|
|
|
|
import org.signal.contacts.SystemContactsRepository
|
2022-08-08 12:22:48 +00:00
|
|
|
import org.signal.core.util.Stopwatch
|
2022-04-11 23:59:17 +00:00
|
|
|
import org.signal.core.util.logging.Log
|
|
|
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
|
|
|
import org.thoughtcrime.securesms.BuildConfig
|
|
|
|
import org.thoughtcrime.securesms.database.RecipientDatabase
|
|
|
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
|
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
|
|
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
|
|
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
|
|
|
import org.thoughtcrime.securesms.recipients.Recipient
|
|
|
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
|
|
|
import org.whispersystems.signalservice.api.push.ServiceId
|
2022-05-18 15:42:38 +00:00
|
|
|
import org.whispersystems.signalservice.api.services.CdsiV2Service
|
2022-04-11 23:59:17 +00:00
|
|
|
import java.io.IOException
|
|
|
|
import java.util.Optional
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Performs the CDS refresh using the V2 interface (either CDSH or CDSI) that returns both PNIs and ACIs.
|
|
|
|
*/
|
|
|
|
object ContactDiscoveryRefreshV2 {
|
|
|
|
|
2022-08-17 22:23:40 +00:00
|
|
|
// Using Log.tag will cut off the version number
|
|
|
|
private const val TAG = "CdsRefreshV2"
|
2022-04-11 23:59:17 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The maximum number items we will allow in a 'one-off' request.
|
|
|
|
* One-off requests, while much faster, will always deduct the request size from our rate limit.
|
|
|
|
* So we need to be careful about making it too large.
|
|
|
|
* If a request size is over this limit, we will always fall back to a full sync.
|
|
|
|
*/
|
|
|
|
private const val MAXIMUM_ONE_OFF_REQUEST_SIZE = 3
|
|
|
|
|
|
|
|
@Throws(IOException::class)
|
|
|
|
@WorkerThread
|
|
|
|
@Synchronized
|
|
|
|
@JvmStatic
|
2022-08-17 22:23:40 +00:00
|
|
|
fun refreshAll(context: Context, ignoreResults: Boolean = false): ContactDiscovery.RefreshResult {
|
2022-04-11 23:59:17 +00:00
|
|
|
val stopwatch = Stopwatch("refresh-all")
|
|
|
|
|
2022-05-18 15:42:38 +00:00
|
|
|
val previousE164s: Set<String> = if (SignalStore.misc().cdsToken != null) {
|
|
|
|
SignalDatabase.cds.getAllE164s()
|
|
|
|
} else {
|
|
|
|
Log.w(TAG, "No token set! Cannot provide previousE164s.")
|
|
|
|
emptySet()
|
|
|
|
}
|
2022-04-11 23:59:17 +00:00
|
|
|
stopwatch.split("previous")
|
|
|
|
|
|
|
|
val recipientE164s: Set<String> = SignalDatabase.recipients.getAllE164s().sanitize()
|
|
|
|
val newRecipientE164s: Set<String> = recipientE164s - previousE164s
|
|
|
|
stopwatch.split("recipient")
|
|
|
|
|
|
|
|
val systemE164s: Set<String> = SystemContactsRepository.getAllDisplayNumbers(context).toE164s(context).sanitize()
|
|
|
|
val newSystemE164s: Set<String> = systemE164s - previousE164s
|
|
|
|
stopwatch.split("system")
|
|
|
|
|
|
|
|
val newE164s: Set<String> = newRecipientE164s + newSystemE164s
|
|
|
|
|
2022-08-17 22:23:40 +00:00
|
|
|
val tokenToUse: ByteArray? = if (previousE164s.isNotEmpty()) {
|
|
|
|
SignalStore.misc().cdsToken
|
|
|
|
} else {
|
|
|
|
if (SignalStore.misc().cdsToken != null) {
|
|
|
|
Log.w(TAG, "We have a token, but our previousE164 list is empty! We cannot provide a token.")
|
|
|
|
}
|
|
|
|
null
|
|
|
|
}
|
|
|
|
|
2022-05-18 15:42:38 +00:00
|
|
|
val response: CdsiV2Service.Response = makeRequest(
|
2022-04-11 23:59:17 +00:00
|
|
|
previousE164s = previousE164s,
|
|
|
|
newE164s = newE164s,
|
|
|
|
serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(),
|
2022-08-17 22:23:40 +00:00
|
|
|
token = tokenToUse,
|
|
|
|
saveToken = true,
|
|
|
|
tag = "refresh-all"
|
2022-04-11 23:59:17 +00:00
|
|
|
)
|
|
|
|
stopwatch.split("network")
|
|
|
|
|
|
|
|
SignalDatabase.cds.updateAfterCdsQuery(newE164s, recipientE164s + systemE164s)
|
|
|
|
stopwatch.split("cds-db")
|
|
|
|
|
2022-08-17 22:23:40 +00:00
|
|
|
var registeredIds: Set<RecipientId> = emptySet()
|
|
|
|
|
|
|
|
if (ignoreResults) {
|
|
|
|
Log.w(TAG, "[refresh-all] Ignoring CDSv2 results.")
|
|
|
|
} else {
|
|
|
|
registeredIds = SignalDatabase.recipients.bulkProcessCdsV2Result(
|
|
|
|
response.results
|
|
|
|
.mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) }
|
|
|
|
)
|
|
|
|
stopwatch.split("recipient-db")
|
2022-04-11 23:59:17 +00:00
|
|
|
|
2022-08-17 22:23:40 +00:00
|
|
|
SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList())
|
|
|
|
stopwatch.split("update-registered")
|
|
|
|
}
|
2022-07-11 19:20:00 +00:00
|
|
|
|
2022-04-11 23:59:17 +00:00
|
|
|
stopwatch.stop(TAG)
|
2022-08-17 22:23:40 +00:00
|
|
|
Log.d(TAG, "[refresh-all] Used ${response.quotaUsedDebugOnly} units of our quota.")
|
2022-04-11 23:59:17 +00:00
|
|
|
|
|
|
|
return ContactDiscovery.RefreshResult(registeredIds, emptyMap())
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(IOException::class)
|
|
|
|
@WorkerThread
|
|
|
|
@Synchronized
|
|
|
|
@JvmStatic
|
2022-08-17 22:23:40 +00:00
|
|
|
fun refresh(context: Context, inputRecipients: List<Recipient>, ignoreResults: Boolean = false): ContactDiscovery.RefreshResult {
|
2022-04-11 23:59:17 +00:00
|
|
|
val stopwatch = Stopwatch("refresh-some")
|
|
|
|
|
|
|
|
val recipients = inputRecipients.map { it.resolve() }
|
|
|
|
stopwatch.split("resolve")
|
|
|
|
|
|
|
|
val inputIds: Set<RecipientId> = recipients.map { it.id }.toSet()
|
|
|
|
val inputE164s: Set<String> = recipients.mapNotNull { it.e164.orElse(null) }.toSet()
|
|
|
|
|
2022-04-25 16:37:25 +00:00
|
|
|
if (inputE164s.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
|
2022-04-11 23:59:17 +00:00
|
|
|
Log.i(TAG, "List of specific recipients to refresh is too large! (Size: ${recipients.size}). Doing a full refresh instead.")
|
2022-08-17 22:23:40 +00:00
|
|
|
val fullResult: ContactDiscovery.RefreshResult = refreshAll(context, ignoreResults)
|
2022-04-11 23:59:17 +00:00
|
|
|
|
|
|
|
return ContactDiscovery.RefreshResult(
|
|
|
|
registeredIds = fullResult.registeredIds.intersect(inputIds),
|
|
|
|
rewrites = fullResult.rewrites.filterKeys { inputE164s.contains(it) }
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-04-25 16:37:25 +00:00
|
|
|
if (inputE164s.isEmpty()) {
|
|
|
|
Log.w(TAG, "No numbers to refresh!")
|
|
|
|
return ContactDiscovery.RefreshResult(emptySet(), emptyMap())
|
|
|
|
} else {
|
|
|
|
Log.i(TAG, "Doing a one-off request for ${inputE164s.size} recipients.")
|
|
|
|
}
|
2022-04-11 23:59:17 +00:00
|
|
|
|
2022-05-18 15:42:38 +00:00
|
|
|
val response: CdsiV2Service.Response = makeRequest(
|
2022-04-11 23:59:17 +00:00
|
|
|
previousE164s = emptySet(),
|
|
|
|
newE164s = inputE164s,
|
2022-05-18 15:42:38 +00:00
|
|
|
serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(),
|
|
|
|
token = null,
|
2022-08-17 22:23:40 +00:00
|
|
|
saveToken = false,
|
|
|
|
tag = "refresh-some"
|
2022-04-11 23:59:17 +00:00
|
|
|
)
|
|
|
|
stopwatch.split("network")
|
|
|
|
|
2022-08-17 22:23:40 +00:00
|
|
|
var registeredIds: Set<RecipientId> = emptySet()
|
2022-04-11 23:59:17 +00:00
|
|
|
|
2022-08-17 22:23:40 +00:00
|
|
|
if (ignoreResults) {
|
|
|
|
Log.w(TAG, "[refresh-some] Ignoring CDSv2 results.")
|
|
|
|
} else {
|
|
|
|
registeredIds = SignalDatabase.recipients.bulkProcessCdsV2Result(
|
|
|
|
response.results
|
|
|
|
.mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) }
|
|
|
|
)
|
|
|
|
stopwatch.split("recipient-db")
|
|
|
|
|
|
|
|
SignalDatabase.recipients.bulkUpdatedRegisteredStatus(registeredIds.associateWith { null }, emptyList())
|
|
|
|
stopwatch.split("update-registered")
|
|
|
|
}
|
2022-07-11 19:20:00 +00:00
|
|
|
|
2022-08-17 22:23:40 +00:00
|
|
|
Log.d(TAG, "[refresh-some] Used ${response.quotaUsedDebugOnly} units of our quota.")
|
2022-04-11 23:59:17 +00:00
|
|
|
stopwatch.stop(TAG)
|
|
|
|
|
|
|
|
return ContactDiscovery.RefreshResult(registeredIds, emptyMap())
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(IOException::class)
|
2022-08-17 22:23:40 +00:00
|
|
|
private fun makeRequest(previousE164s: Set<String>, newE164s: Set<String>, serviceIds: Map<ServiceId, ProfileKey>, token: ByteArray?, saveToken: Boolean, tag: String): CdsiV2Service.Response {
|
2022-05-18 15:42:38 +00:00
|
|
|
return ApplicationDependencies.getSignalServiceAccountManager().getRegisteredUsersWithCdsi(
|
2022-04-11 23:59:17 +00:00
|
|
|
previousE164s,
|
|
|
|
newE164s,
|
|
|
|
serviceIds,
|
2022-05-18 15:42:38 +00:00
|
|
|
Optional.ofNullable(token),
|
|
|
|
BuildConfig.CDSI_MRENCLAVE
|
2022-08-17 22:23:40 +00:00
|
|
|
) { tokenToSave ->
|
2022-05-18 15:42:38 +00:00
|
|
|
if (saveToken) {
|
2022-08-17 22:23:40 +00:00
|
|
|
SignalStore.misc().cdsToken = tokenToSave
|
|
|
|
Log.d(TAG, "[$tag] Token saved!")
|
2022-05-18 15:42:38 +00:00
|
|
|
}
|
|
|
|
}
|
2022-04-11 23:59:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun Set<String>.toE164s(context: Context): Set<String> {
|
|
|
|
return this.map { PhoneNumberFormatter.get(context).format(it) }.toSet()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun Set<String>.sanitize(): Set<String> {
|
|
|
|
return this
|
|
|
|
.filter {
|
|
|
|
try {
|
|
|
|
it.startsWith("+") && it.length > 1 && it[1] != '0' && it.toLong() > 0
|
|
|
|
} catch (e: NumberFormatException) {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.toSet()
|
|
|
|
}
|
|
|
|
}
|