Basic client usage of CDSHv2.
This provides a basic (read: useful-for-development-yet-broken) client usage of CDSHv2.
This commit is contained in:
parent
b0e7b49056
commit
d3096c56cb
13 changed files with 457 additions and 17 deletions
|
@ -406,7 +406,28 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||||
|
|
||||||
dividerPref()
|
dividerPref()
|
||||||
|
|
||||||
|
sectionHeaderPref(R.string.preferences__internal_cds)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_clear_history),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_clear_history_description),
|
||||||
|
onClick = {
|
||||||
|
clearCdsHistory()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
clickPref(
|
||||||
|
title = DSLSettingsText.from(R.string.preferences__internal_clear_all_service_ids),
|
||||||
|
summary = DSLSettingsText.from(R.string.preferences__internal_clear_all_service_ids_description),
|
||||||
|
onClick = {
|
||||||
|
clearAllServiceIds()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
dividerPref()
|
||||||
|
|
||||||
sectionHeaderPref(R.string.ConversationListTabs__stories)
|
sectionHeaderPref(R.string.ConversationListTabs__stories)
|
||||||
|
|
||||||
switchPref(
|
switchPref(
|
||||||
title = DSLSettingsText.from(R.string.preferences__internal_disable_stories),
|
title = DSLSettingsText.from(R.string.preferences__internal_disable_stories),
|
||||||
isChecked = state.disableStories,
|
isChecked = state.disableStories,
|
||||||
|
@ -518,4 +539,23 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||||
private fun enqueueSubscriptionRedemption() {
|
private fun enqueueSubscriptionRedemption() {
|
||||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue()
|
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun clearCdsHistory() {
|
||||||
|
SignalDatabase.cds.clearAll()
|
||||||
|
SignalStore.misc().cdsToken = null
|
||||||
|
Toast.makeText(context, "Cleared all CDS history.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearAllServiceIds() {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setMessage("Are you sure? Never do this on a non-test device.")
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
SignalDatabase.recipients.debugClearServiceIds()
|
||||||
|
Toast.makeText(context, "Cleared all service IDs.", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { d, _ ->
|
||||||
|
d.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||||
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage
|
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch
|
import org.thoughtcrime.securesms.util.Stopwatch
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
|
@ -69,7 +70,11 @@ object ContactDiscovery {
|
||||||
context = context,
|
context = context,
|
||||||
descriptor = "refresh-all",
|
descriptor = "refresh-all",
|
||||||
refresh = {
|
refresh = {
|
||||||
|
if (FeatureFlags.usePnpCds()) {
|
||||||
|
ContactDiscoveryRefreshV2.refreshAll(context)
|
||||||
|
} else {
|
||||||
ContactDiscoveryRefreshV1.refreshAll(context)
|
ContactDiscoveryRefreshV1.refreshAll(context)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeSystemContactLinksIfMissing = true,
|
removeSystemContactLinksIfMissing = true,
|
||||||
notifyOfNewUsers = notifyOfNewUsers
|
notifyOfNewUsers = notifyOfNewUsers
|
||||||
|
@ -86,7 +91,11 @@ object ContactDiscovery {
|
||||||
context = context,
|
context = context,
|
||||||
descriptor = "refresh-multiple",
|
descriptor = "refresh-multiple",
|
||||||
refresh = {
|
refresh = {
|
||||||
|
if (FeatureFlags.usePnpCds()) {
|
||||||
|
ContactDiscoveryRefreshV2.refresh(context, recipients)
|
||||||
|
} else {
|
||||||
ContactDiscoveryRefreshV1.refresh(context, recipients)
|
ContactDiscoveryRefreshV1.refresh(context, recipients)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeSystemContactLinksIfMissing = false,
|
removeSystemContactLinksIfMissing = false,
|
||||||
notifyOfNewUsers = notifyOfNewUsers
|
notifyOfNewUsers = notifyOfNewUsers
|
||||||
|
@ -101,7 +110,11 @@ object ContactDiscovery {
|
||||||
context = context,
|
context = context,
|
||||||
descriptor = "refresh-single",
|
descriptor = "refresh-single",
|
||||||
refresh = {
|
refresh = {
|
||||||
|
if (FeatureFlags.usePnpCds()) {
|
||||||
|
ContactDiscoveryRefreshV2.refresh(context, listOf(recipient))
|
||||||
|
} else {
|
||||||
ContactDiscoveryRefreshV1.refresh(context, listOf(recipient))
|
ContactDiscoveryRefreshV1.refresh(context, listOf(recipient))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeSystemContactLinksIfMissing = false,
|
removeSystemContactLinksIfMissing = false,
|
||||||
notifyOfNewUsers = notifyOfNewUsers
|
notifyOfNewUsers = notifyOfNewUsers
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
package org.thoughtcrime.securesms.contacts.sync
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import org.signal.contacts.SystemContactsRepository
|
||||||
|
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.thoughtcrime.securesms.util.Stopwatch
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId
|
||||||
|
import org.whispersystems.signalservice.api.services.CdshV2Service
|
||||||
|
import java.io.IOException
|
||||||
|
import java.lang.NumberFormatException
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the CDS refresh using the V2 interface (either CDSH or CDSI) that returns both PNIs and ACIs.
|
||||||
|
*/
|
||||||
|
object ContactDiscoveryRefreshV2 {
|
||||||
|
|
||||||
|
private val TAG = Log.tag(ContactDiscoveryRefreshV2::class.java)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
fun refreshAll(context: Context): ContactDiscovery.RefreshResult {
|
||||||
|
val stopwatch = Stopwatch("refresh-all")
|
||||||
|
|
||||||
|
val previousE164s: Set<String> = SignalDatabase.cds.getAllE164s()
|
||||||
|
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
|
||||||
|
|
||||||
|
val response: CdshV2Service.Response = makeRequest(
|
||||||
|
previousE164s = previousE164s,
|
||||||
|
newE164s = newE164s,
|
||||||
|
serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs(),
|
||||||
|
)
|
||||||
|
stopwatch.split("network")
|
||||||
|
|
||||||
|
SignalStore.misc().cdsToken = response.token
|
||||||
|
SignalDatabase.cds.updateAfterCdsQuery(newE164s, recipientE164s + systemE164s)
|
||||||
|
stopwatch.split("cds-db")
|
||||||
|
|
||||||
|
val registeredIds: Set<RecipientId> = SignalDatabase.recipients.bulkProcessCdsV2Result(
|
||||||
|
response.results
|
||||||
|
.mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) }
|
||||||
|
)
|
||||||
|
stopwatch.split("recipient-db")
|
||||||
|
|
||||||
|
stopwatch.stop(TAG)
|
||||||
|
|
||||||
|
return ContactDiscovery.RefreshResult(registeredIds, emptyMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
@WorkerThread
|
||||||
|
@Synchronized
|
||||||
|
@JvmStatic
|
||||||
|
fun refresh(context: Context, inputRecipients: List<Recipient>): ContactDiscovery.RefreshResult {
|
||||||
|
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()
|
||||||
|
|
||||||
|
if (recipients.size > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
|
||||||
|
Log.i(TAG, "List of specific recipients to refresh is too large! (Size: ${recipients.size}). Doing a full refresh instead.")
|
||||||
|
val fullResult: ContactDiscovery.RefreshResult = refreshAll(context)
|
||||||
|
|
||||||
|
return ContactDiscovery.RefreshResult(
|
||||||
|
registeredIds = fullResult.registeredIds.intersect(inputIds),
|
||||||
|
rewrites = fullResult.rewrites.filterKeys { inputE164s.contains(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Doing a one-off request for ${recipients.size} recipients.")
|
||||||
|
|
||||||
|
val response: CdshV2Service.Response = makeRequest(
|
||||||
|
previousE164s = emptySet(),
|
||||||
|
newE164s = inputE164s,
|
||||||
|
serviceIds = SignalDatabase.recipients.getAllServiceIdProfileKeyPairs()
|
||||||
|
)
|
||||||
|
stopwatch.split("network")
|
||||||
|
|
||||||
|
val registeredIds: Set<RecipientId> = SignalDatabase.recipients.bulkProcessCdsV2Result(
|
||||||
|
response.results
|
||||||
|
.mapValues { entry -> RecipientDatabase.CdsV2Result(entry.value.pni, entry.value.aci.orElse(null)) }
|
||||||
|
)
|
||||||
|
stopwatch.split("recipient-db")
|
||||||
|
|
||||||
|
stopwatch.stop(TAG)
|
||||||
|
|
||||||
|
return ContactDiscovery.RefreshResult(registeredIds, emptyMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun makeRequest(previousE164s: Set<String>, newE164s: Set<String>, serviceIds: Map<ServiceId, ProfileKey>): CdshV2Service.Response {
|
||||||
|
return ApplicationDependencies.getSignalServiceAccountManager().getRegisteredUsersWithCdshV2(
|
||||||
|
previousE164s,
|
||||||
|
newE164s,
|
||||||
|
serviceIds,
|
||||||
|
Optional.ofNullable(SignalStore.misc().cdsToken),
|
||||||
|
BuildConfig.CDSH_PUBLIC_KEY,
|
||||||
|
BuildConfig.CDSH_CODE_HASH
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,10 @@ import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -40,6 +42,27 @@ public final class ProfileKeyUtil {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static @Nullable ProfileKey profileKeyOrNull(@Nullable String base64) {
|
||||||
|
if (base64 == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] decoded;
|
||||||
|
try {
|
||||||
|
decoded = Base64.decode(base64);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "Failed to decode profile key.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new ProfileKey(decoded);
|
||||||
|
} catch (InvalidInputException e) {
|
||||||
|
Log.w(TAG, String.format(Locale.US, "Seen non-null profile key of wrong length %d", decoded.length), e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static @Nullable ProfileKeyCredential profileKeyCredentialOrNull(@Nullable byte[] profileKeyCredential) {
|
public static @Nullable ProfileKeyCredential profileKeyCredentialOrNull(@Nullable byte[] profileKeyCredential) {
|
||||||
if (profileKeyCredential != null) {
|
if (profileKeyCredential != null) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
package org.thoughtcrime.securesms.database
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.contentValuesOf
|
||||||
|
import org.signal.core.util.SqlUtil
|
||||||
|
import org.signal.core.util.delete
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.requireNonNullString
|
||||||
|
import org.signal.core.util.select
|
||||||
|
import org.signal.core.util.update
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of the numbers we've previously queried CDS for.
|
||||||
|
*
|
||||||
|
* This is important for rate-limiting: our rate-limiting strategy hinges on keeping
|
||||||
|
* an accurate history of numbers we've queried so that we're only "charged" for
|
||||||
|
* querying new numbers.
|
||||||
|
*/
|
||||||
|
class CdsDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) {
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(CdsDatabase::class.java)
|
||||||
|
|
||||||
|
const val TABLE_NAME = "cds"
|
||||||
|
|
||||||
|
private const val ID = "_id"
|
||||||
|
const val E164 = "e164"
|
||||||
|
private const val LAST_SEEN_AT = "last_seen_at"
|
||||||
|
|
||||||
|
const val CREATE_TABLE = """
|
||||||
|
CREATE TABLE $TABLE_NAME (
|
||||||
|
$ID INTEGER PRIMARY KEY,
|
||||||
|
$E164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE,
|
||||||
|
$LAST_SEEN_AT INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllE164s(): Set<String> {
|
||||||
|
val e164s: MutableSet<String> = mutableSetOf()
|
||||||
|
|
||||||
|
readableDatabase
|
||||||
|
.select(E164)
|
||||||
|
.from(TABLE_NAME)
|
||||||
|
.run()
|
||||||
|
.use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
e164s += cursor.requireNonNullString(E164)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e164s
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param newE164s The newly-added E164s that we hadn't previously queried for.
|
||||||
|
* @param seenE164s The E164s that were seen in either the system contacts or recipients table.
|
||||||
|
* This should be a superset of [newE164s]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun updateAfterCdsQuery(newE164s: Set<String>, seenE164s: Set<String>) {
|
||||||
|
val lastSeen = System.currentTimeMillis()
|
||||||
|
|
||||||
|
writableDatabase.beginTransaction()
|
||||||
|
try {
|
||||||
|
val insertValues: List<ContentValues> = newE164s.map { contentValuesOf(E164 to it) }
|
||||||
|
|
||||||
|
SqlUtil.buildBulkInsert(TABLE_NAME, arrayOf(E164), insertValues)
|
||||||
|
.forEach { writableDatabase.execSQL(it.where, it.whereArgs) }
|
||||||
|
|
||||||
|
for (e164 in seenE164s) {
|
||||||
|
writableDatabase
|
||||||
|
.update(TABLE_NAME)
|
||||||
|
.values(LAST_SEEN_AT to lastSeen)
|
||||||
|
.where("$E164 = ?", e164)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
writableDatabase.setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
writableDatabase.endTransaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearAll() {
|
||||||
|
writableDatabase
|
||||||
|
.delete(TABLE_NAME)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,8 @@ import org.signal.core.util.requireInt
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.signal.core.util.requireString
|
import org.signal.core.util.requireString
|
||||||
|
import org.signal.core.util.select
|
||||||
|
import org.signal.core.util.update
|
||||||
import org.signal.libsignal.protocol.IdentityKey
|
import org.signal.libsignal.protocol.IdentityKey
|
||||||
import org.signal.libsignal.protocol.InvalidKeyException
|
import org.signal.libsignal.protocol.InvalidKeyException
|
||||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||||
|
@ -237,7 +239,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||||
|
|
||||||
val CREATE_INDEXS = arrayOf(
|
val CREATE_INDEXS = arrayOf(
|
||||||
"CREATE INDEX IF NOT EXISTS recipient_group_type_index ON $TABLE_NAME ($GROUP_TYPE);",
|
"CREATE INDEX IF NOT EXISTS recipient_group_type_index ON $TABLE_NAME ($GROUP_TYPE);",
|
||||||
"CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON $TABLE_NAME ($PNI_COLUMN)"
|
"CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON $TABLE_NAME ($PNI_COLUMN)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS recipient_service_id_profile_key ON $TABLE_NAME ($SERVICE_ID, $PROFILE_KEY) WHERE $SERVICE_ID NOT NULL AND $PROFILE_KEY NOT NULL"
|
||||||
)
|
)
|
||||||
|
|
||||||
private val RECIPIENT_PROJECTION: Array<String> = arrayOf(
|
private val RECIPIENT_PROJECTION: Array<String> = arrayOf(
|
||||||
|
@ -499,6 +502,28 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAllServiceIdProfileKeyPairs(): Map<ServiceId, ProfileKey> {
|
||||||
|
val serviceIdToProfileKey: MutableMap<ServiceId, ProfileKey> = mutableMapOf()
|
||||||
|
|
||||||
|
readableDatabase
|
||||||
|
.select(SERVICE_ID, PROFILE_KEY)
|
||||||
|
.from(TABLE_NAME)
|
||||||
|
.where("$SERVICE_ID NOT NULL AND $PROFILE_KEY NOT NULL")
|
||||||
|
.run()
|
||||||
|
.use { cursor ->
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val serviceId: ServiceId? = ServiceId.parseOrNull(cursor.requireString(SERVICE_ID))
|
||||||
|
val profileKey: ProfileKey? = ProfileKeyUtil.profileKeyOrNull(cursor.requireString(PROFILE_KEY))
|
||||||
|
|
||||||
|
if (serviceId != null && profileKey != null) {
|
||||||
|
serviceIdToProfileKey[serviceId] = profileKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceIdToProfileKey
|
||||||
|
}
|
||||||
|
|
||||||
private fun fetchRecipient(serviceId: ServiceId?, e164: String?, highTrust: Boolean, changeSelf: Boolean): RecipientFetch {
|
private fun fetchRecipient(serviceId: ServiceId?, e164: String?, highTrust: Boolean, changeSelf: Boolean): RecipientFetch {
|
||||||
val byE164 = e164?.let { getByE164(it) } ?: Optional.empty()
|
val byE164 = e164?.let { getByE164(it) } ?: Optional.empty()
|
||||||
val byAci = serviceId?.let { getByServiceId(it) } ?: Optional.empty()
|
val byAci = serviceId?.let { getByServiceId(it) } ?: Optional.empty()
|
||||||
|
@ -2087,6 +2112,27 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||||
return aciMap
|
return aciMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dumb implementation of processing CDSv2 results. Suitable only for testing and not for actual use.
|
||||||
|
*/
|
||||||
|
fun bulkProcessCdsV2Result(mapping: Map<String, CdsV2Result>): Set<RecipientId> {
|
||||||
|
val ids: MutableSet<RecipientId> = mutableSetOf()
|
||||||
|
val db = writableDatabase
|
||||||
|
|
||||||
|
db.beginTransaction()
|
||||||
|
try {
|
||||||
|
for ((e164, result) in mapping) {
|
||||||
|
ids += getAndPossiblyMerge(result.bestServiceId(), e164, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.setTransactionSuccessful()
|
||||||
|
} finally {
|
||||||
|
db.endTransaction()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
fun getUninvitedRecipientsForInsights(): List<RecipientId> {
|
fun getUninvitedRecipientsForInsights(): List<RecipientId> {
|
||||||
val results: MutableList<RecipientId> = LinkedList()
|
val results: MutableList<RecipientId> = LinkedList()
|
||||||
val args = arrayOf((System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)).toString())
|
val args = arrayOf((System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)).toString())
|
||||||
|
@ -2876,6 +2922,19 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should only be used for debugging! A very destructive action that clears all known serviceIds.
|
||||||
|
*/
|
||||||
|
fun debugClearServiceIds() {
|
||||||
|
writableDatabase
|
||||||
|
.update(TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
SERVICE_ID to null,
|
||||||
|
PNI_COLUMN to null
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
fun getRecord(context: Context, cursor: Cursor): RecipientRecord {
|
fun getRecord(context: Context, cursor: Cursor): RecipientRecord {
|
||||||
return getRecord(context, cursor, ID)
|
return getRecord(context, cursor, ID)
|
||||||
}
|
}
|
||||||
|
@ -3431,4 +3490,17 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
|
||||||
val serviceId: ServiceId? = null,
|
val serviceId: ServiceId? = null,
|
||||||
val e164: String? = null
|
val e164: String? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class CdsV2Result(
|
||||||
|
val pni: PNI,
|
||||||
|
val aci: ACI?
|
||||||
|
) {
|
||||||
|
fun bestServiceId(): ServiceId {
|
||||||
|
return if (aci != null) {
|
||||||
|
aci
|
||||||
|
} else {
|
||||||
|
pni
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
val donationReceiptDatabase: DonationReceiptDatabase = DonationReceiptDatabase(context, this)
|
val donationReceiptDatabase: DonationReceiptDatabase = DonationReceiptDatabase(context, this)
|
||||||
val distributionListDatabase: DistributionListDatabase = DistributionListDatabase(context, this)
|
val distributionListDatabase: DistributionListDatabase = DistributionListDatabase(context, this)
|
||||||
val storySendsDatabase: StorySendsDatabase = StorySendsDatabase(context, this)
|
val storySendsDatabase: StorySendsDatabase = StorySendsDatabase(context, this)
|
||||||
|
val cdsDatabase: CdsDatabase = CdsDatabase(context, this)
|
||||||
|
|
||||||
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||||
db.enableWriteAheadLogging()
|
db.enableWriteAheadLogging()
|
||||||
|
@ -105,6 +106,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
db.execSQL(ReactionDatabase.CREATE_TABLE)
|
db.execSQL(ReactionDatabase.CREATE_TABLE)
|
||||||
db.execSQL(DonationReceiptDatabase.CREATE_TABLE)
|
db.execSQL(DonationReceiptDatabase.CREATE_TABLE)
|
||||||
db.execSQL(StorySendsDatabase.CREATE_TABLE)
|
db.execSQL(StorySendsDatabase.CREATE_TABLE)
|
||||||
|
db.execSQL(CdsDatabase.CREATE_TABLE)
|
||||||
executeStatements(db, SearchDatabase.CREATE_TABLE)
|
executeStatements(db, SearchDatabase.CREATE_TABLE)
|
||||||
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
|
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE)
|
||||||
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
|
executeStatements(db, MessageSendLogDatabase.CREATE_TABLE)
|
||||||
|
@ -328,6 +330,11 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
val avatarPicker: AvatarPickerDatabase
|
val avatarPicker: AvatarPickerDatabase
|
||||||
get() = instance!!.avatarPickerDatabase
|
get() = instance!!.avatarPickerDatabase
|
||||||
|
|
||||||
|
@get:JvmStatic
|
||||||
|
@get:JvmName("cds")
|
||||||
|
val cds: CdsDatabase
|
||||||
|
get() = instance!!.cdsDatabase
|
||||||
|
|
||||||
@get:JvmStatic
|
@get:JvmStatic
|
||||||
@get:JvmName("chatColors")
|
@get:JvmName("chatColors")
|
||||||
val chatColors: ChatColorsDatabase
|
val chatColors: ChatColorsDatabase
|
||||||
|
@ -338,6 +345,11 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
val distributionLists: DistributionListDatabase
|
val distributionLists: DistributionListDatabase
|
||||||
get() = instance!!.distributionListDatabase
|
get() = instance!!.distributionListDatabase
|
||||||
|
|
||||||
|
@get:JvmStatic
|
||||||
|
@get:JvmName("donationReceipts")
|
||||||
|
val donationReceipts: DonationReceiptDatabase
|
||||||
|
get() = instance!!.donationReceiptDatabase
|
||||||
|
|
||||||
@get:JvmStatic
|
@get:JvmStatic
|
||||||
@get:JvmName("drafts")
|
@get:JvmName("drafts")
|
||||||
val drafts: DraftDatabase
|
val drafts: DraftDatabase
|
||||||
|
@ -474,19 +486,14 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||||
val stickers: StickerDatabase
|
val stickers: StickerDatabase
|
||||||
get() = instance!!.stickerDatabase
|
get() = instance!!.stickerDatabase
|
||||||
|
|
||||||
@get:JvmStatic
|
|
||||||
@get:JvmName("unknownStorageIds")
|
|
||||||
val unknownStorageIds: UnknownStorageIdDatabase
|
|
||||||
get() = instance!!.storageIdDatabase
|
|
||||||
|
|
||||||
@get:JvmStatic
|
|
||||||
@get:JvmName("donationReceipts")
|
|
||||||
val donationReceipts: DonationReceiptDatabase
|
|
||||||
get() = instance!!.donationReceiptDatabase
|
|
||||||
|
|
||||||
@get:JvmStatic
|
@get:JvmStatic
|
||||||
@get:JvmName("storySends")
|
@get:JvmName("storySends")
|
||||||
val storySends: StorySendsDatabase
|
val storySends: StorySendsDatabase
|
||||||
get() = instance!!.storySendsDatabase
|
get() = instance!!.storySendsDatabase
|
||||||
|
|
||||||
|
@get:JvmStatic
|
||||||
|
@get:JvmName("unknownStorageIds")
|
||||||
|
val unknownStorageIds: UnknownStorageIdDatabase
|
||||||
|
get() = instance!!.storageIdDatabase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,8 +193,9 @@ object SignalDatabaseMigrations {
|
||||||
private const val STORY_TYPE_AND_DISTRIBUTION = 137
|
private const val STORY_TYPE_AND_DISTRIBUTION = 137
|
||||||
private const val CLEAN_DELETED_DISTRIBUTION_LISTS = 138
|
private const val CLEAN_DELETED_DISTRIBUTION_LISTS = 138
|
||||||
private const val REMOVE_KNOWN_UNKNOWNS = 139
|
private const val REMOVE_KNOWN_UNKNOWNS = 139
|
||||||
|
private const val CDS_V2 = 140
|
||||||
|
|
||||||
const val DATABASE_VERSION = 139
|
const val DATABASE_VERSION = 140
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
@ -2509,6 +2510,19 @@ object SignalDatabaseMigrations {
|
||||||
val count: Int = db.delete("storage_key", "type <= ?", SqlUtil.buildArgs(4))
|
val count: Int = db.delete("storage_key", "type <= ?", SqlUtil.buildArgs(4))
|
||||||
Log.i(TAG, "Cleaned up $count invalid unknown records.")
|
Log.i(TAG, "Cleaned up $count invalid unknown records.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (oldVersion < CDS_V2) {
|
||||||
|
db.execSQL("CREATE INDEX IF NOT EXISTS recipient_service_id_profile_key ON recipient (uuid, profile_key) WHERE uuid NOT NULL AND profile_key NOT NULL")
|
||||||
|
db.execSQL(
|
||||||
|
"""
|
||||||
|
CREATE TABLE cds (
|
||||||
|
_id INTEGER PRIMARY KEY,
|
||||||
|
e164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE,
|
||||||
|
last_seen_at INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||||
import org.thoughtcrime.securesms.util.Base64;
|
import org.thoughtcrime.securesms.util.Base64;
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||||
import org.signal.core.util.SetUtil;
|
import org.signal.core.util.SetUtil;
|
||||||
|
@ -306,7 +307,8 @@ public class RetrieveProfileJob extends BaseJob {
|
||||||
});
|
});
|
||||||
|
|
||||||
recipientDatabase.markProfilesFetched(success, System.currentTimeMillis());
|
recipientDatabase.markProfilesFetched(success, System.currentTimeMillis());
|
||||||
if (operationState.unregistered.size() > 0 || newlyRegistered.size() > 0) {
|
// XXX The service hasn't implemented profiles for PNIs yet, so if using PNP CDS we don't want to mark users without profiles as unregistered.
|
||||||
|
if ((operationState.unregistered.size() > 0 || newlyRegistered.size() > 0) && !FeatureFlags.usePnpCds()) {
|
||||||
Log.i(TAG, "Marking " + newlyRegistered.size() + " users as registered and " + operationState.unregistered.size() + " users as unregistered.");
|
Log.i(TAG, "Marking " + newlyRegistered.size() + " users as registered and " + operationState.unregistered.size() + " users as unregistered.");
|
||||||
recipientDatabase.bulkUpdatedRegisteredStatus(newlyRegistered, operationState.unregistered);
|
recipientDatabase.bulkUpdatedRegisteredStatus(newlyRegistered, operationState.unregistered);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.keyvalue;
|
package org.thoughtcrime.securesms.keyvalue;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -19,6 +20,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||||
private static final String CENSORSHIP_LAST_CHECK_TIME = "misc.censorship.last_check_time";
|
private static final String CENSORSHIP_LAST_CHECK_TIME = "misc.censorship.last_check_time";
|
||||||
private static final String CENSORSHIP_SERVICE_REACHABLE = "misc.censorship.service_reachable";
|
private static final String CENSORSHIP_SERVICE_REACHABLE = "misc.censorship.service_reachable";
|
||||||
private static final String LAST_GV2_PROFILE_CHECK_TIME = "misc.last_gv2_profile_check_time";
|
private static final String LAST_GV2_PROFILE_CHECK_TIME = "misc.last_gv2_profile_check_time";
|
||||||
|
private static final String CDS_TOKEN = "misc.cds_token";
|
||||||
|
|
||||||
MiscellaneousValues(@NonNull KeyValueStore store) {
|
MiscellaneousValues(@NonNull KeyValueStore store) {
|
||||||
super(store);
|
super(store);
|
||||||
|
@ -137,4 +139,14 @@ public final class MiscellaneousValues extends SignalStoreValues {
|
||||||
public void setLastGv2ProfileCheckTime(long value) {
|
public void setLastGv2ProfileCheckTime(long value) {
|
||||||
putLong(LAST_GV2_PROFILE_CHECK_TIME, value);
|
putLong(LAST_GV2_PROFILE_CHECK_TIME, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public @Nullable byte[] getCdsToken() {
|
||||||
|
return getBlob(CDS_TOKEN, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCdsToken(@Nullable byte[] token) {
|
||||||
|
getStore().beginWrite()
|
||||||
|
.putBlob(CDS_TOKEN, token)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,7 @@ public final class FeatureFlags {
|
||||||
private static final String USE_HARDWARE_AEC_IF_OLD = "android.calling.useHardwareAecIfOlderThanApi29";
|
private static final String USE_HARDWARE_AEC_IF_OLD = "android.calling.useHardwareAecIfOlderThanApi29";
|
||||||
private static final String USE_AEC3 = "android.calling.useAec3";
|
private static final String USE_AEC3 = "android.calling.useAec3";
|
||||||
private static final String PAYMENTS_COUNTRY_BLOCKLIST = "android.payments.blocklist";
|
private static final String PAYMENTS_COUNTRY_BLOCKLIST = "android.payments.blocklist";
|
||||||
|
private static final String PNP_CDS = "android.pnp.cds";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||||
|
@ -147,7 +148,8 @@ public final class FeatureFlags {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(
|
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(
|
||||||
PHONE_NUMBER_PRIVACY_VERSION
|
PHONE_NUMBER_PRIVACY_VERSION,
|
||||||
|
PNP_CDS
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -494,6 +496,15 @@ public final class FeatureFlags {
|
||||||
return getBoolean(USE_AEC3, true);
|
return getBoolean(USE_AEC3, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to use the phone number privacy CDS flow. Only currently works in staging.
|
||||||
|
*
|
||||||
|
* Note: This feature is in very early stages of development and *will* break your contacts.
|
||||||
|
*/
|
||||||
|
public static boolean usePnpCds() {
|
||||||
|
return Environment.IS_STAGING && getBoolean(PNP_CDS, false);
|
||||||
|
}
|
||||||
|
|
||||||
/** Only for rendering debug info. */
|
/** Only for rendering debug info. */
|
||||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||||
return new TreeMap<>(REMOTE_VALUES);
|
return new TreeMap<>(REMOTE_VALUES);
|
||||||
|
|
|
@ -2709,6 +2709,11 @@
|
||||||
<string name="preferences__internal_release_channel_set_last_version" translatable="false">Set last version seen back 10 versions</string>
|
<string name="preferences__internal_release_channel_set_last_version" translatable="false">Set last version seen back 10 versions</string>
|
||||||
<string name="preferences__internal_add_sample_note" translatable="false">Add sample note</string>
|
<string name="preferences__internal_add_sample_note" translatable="false">Add sample note</string>
|
||||||
<string name="preferences__internal_disable_stories" translatable="false">Disable stories</string>
|
<string name="preferences__internal_disable_stories" translatable="false">Disable stories</string>
|
||||||
|
<string name="preferences__internal_cds" translatable="false">CDS</string>
|
||||||
|
<string name="preferences__internal_clear_history" translatable="false">Clear history</string>
|
||||||
|
<string name="preferences__internal_clear_history_description" translatable="false">Clears all CDS history, meaning the next sync will consider all numbers to be new.</string>
|
||||||
|
<string name="preferences__internal_clear_all_service_ids" translatable="false">Clear all service IDs</string>
|
||||||
|
<string name="preferences__internal_clear_all_service_ids_description" translatable="false">Clears all known service IDs. Do not use on your personal device!</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Payments -->
|
<!-- Payments -->
|
||||||
|
|
|
@ -224,3 +224,4 @@ class DeleteBuilderPart2(
|
||||||
return db.delete(tableName, where, whereArgs)
|
return db.delete(tableName, where, whereArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue