diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 0e22a73cd5..8bd56bc5e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -385,6 +385,20 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da /** Used as a placeholder recipient for self during migrations when self isn't yet available. */ private val PLACEHOLDER_SELF_ID = -2L + + @JvmStatic + fun maskCapabilitiesToLong(capabilities: SignalServiceProfile.Capabilities): Long { + var value: Long = 0 + value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration).serialize().toLong()) + value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey).serialize().toLong()) + value = Bitmask.update(value, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isAnnouncementGroup).serialize().toLong()) + value = Bitmask.update(value, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isChangeNumber).serialize().toLong()) + value = Bitmask.update(value, Capabilities.STORIES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isStories).serialize().toLong()) + value = Bitmask.update(value, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGiftBadges).serialize().toLong()) + value = Bitmask.update(value, Capabilities.PNP, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPnp).serialize().toLong()) + value = Bitmask.update(value, Capabilities.PAYMENT_ACTIVATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPaymentActivation).serialize().toLong()) + return value + } } fun getByE164(e164: String): Optional { @@ -675,6 +689,24 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return RecipientReader(cursor) } + fun getRecords(ids: Collection): Map { + val queries = SqlUtil.buildCollectionQuery( + column = ID, + values = ids.map { it.serialize() } + ) + + val foundRecords = queries.flatMap { query -> + readableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, null).readToList { cursor -> + getRecord(context, cursor) + } + } + + val foundIds = foundRecords.map { record -> record.id } + val remappedRecords = ids.filterNot { it in foundIds }.map(::findRemappedIdRecord) + + return (foundRecords + remappedRecords).associateBy { it.id } + } + fun getRecord(id: RecipientId): RecipientRecord { val query = "$ID = ?" val args = arrayOf(id.serialize()) @@ -683,18 +715,22 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return if (cursor != null && cursor.moveToNext()) { getRecord(context, cursor) } else { - val remapped = RemappedRecords.getInstance().getRecipient(id) - - if (remapped.isPresent) { - Log.w(TAG, "Missing recipient for $id, but found it in the remapped records as ${remapped.get()}") - getRecord(remapped.get()) - } else { - throw MissingRecipientException(id) - } + findRemappedIdRecord(id) } } } + private fun findRemappedIdRecord(id: RecipientId): RecipientRecord { + val remapped = RemappedRecords.getInstance().getRecipient(id) + + return if (remapped.isPresent) { + Log.w(TAG, "Missing recipient for $id, but found it in the remapped records as ${remapped.get()}") + getRecord(remapped.get()) + } else { + throw MissingRecipientException(id) + } + } + fun getRecordForSync(id: RecipientId): RecipientRecord? { val query = "$TABLE_NAME.$ID = ?" val args = arrayOf(id.serialize()) @@ -1457,18 +1493,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } fun setCapabilities(id: RecipientId, capabilities: SignalServiceProfile.Capabilities) { - var value: Long = 0 - value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration).serialize().toLong()) - value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey).serialize().toLong()) - value = Bitmask.update(value, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isAnnouncementGroup).serialize().toLong()) - value = Bitmask.update(value, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isChangeNumber).serialize().toLong()) - value = Bitmask.update(value, Capabilities.STORIES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isStories).serialize().toLong()) - value = Bitmask.update(value, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGiftBadges).serialize().toLong()) - value = Bitmask.update(value, Capabilities.PNP, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPnp).serialize().toLong()) - value = Bitmask.update(value, Capabilities.PAYMENT_ACTIVATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPaymentActivation).serialize().toLong()) - val values = ContentValues(1).apply { - put(CAPABILITIES, value) + put(CAPABILITIES, maskCapabilitiesToLong(capabilities)) } if (update(id, values)) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 37a946af20..152c838146 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -10,7 +10,9 @@ import androidx.annotation.WorkerThread; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; +import com.annimon.stream.function.Predicate; +import org.signal.core.util.Base64; import org.signal.core.util.ListUtil; import org.signal.core.util.SetUtil; import org.signal.core.util.Stopwatch; @@ -28,10 +30,11 @@ import org.thoughtcrime.securesms.database.GroupTable; import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode; import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.database.model.RecipientRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobmanager.JsonJobData; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.notifications.v2.ConversationId; @@ -40,7 +43,6 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.transport.RetryLaterException; -import org.signal.core.util.Base64; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.ProfileUtil; import org.thoughtcrime.securesms.util.Util; @@ -58,6 +60,8 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -281,6 +285,12 @@ public class RetrieveProfileJob extends BaseJob { stopwatch.split("responses"); + final Map localRecords = SignalDatabase.recipients().getRecords(recipientIds); + + Log.d(TAG, "Fetched " + localRecords.size() + " existing records."); + + stopwatch.split("disk-fetch"); + Set success = SetUtil.difference(recipientIds, operationState.retries); Set newlyRegistered = Stream.of(operationState.profiles) @@ -289,16 +299,41 @@ public class RetrieveProfileJob extends BaseJob { .map(Recipient::getId) .collect(Collectors.toSet()); + List> updatedProfiles = Stream.of(operationState.profiles) + .filter(recipientProfileAndCredentialPair -> { + final Recipient recipientToUpdate = recipientProfileAndCredentialPair.first(); + final RecipientId recipientToUpdateId = recipientToUpdate.getId(); - //noinspection SimplifyStreamApiCallChains - ListUtil.chunk(operationState.profiles, 150).stream().forEach(list -> { - SignalDatabase.runInTransaction((db) -> { - for (Pair profile : list) { - process(profile.first(), profile.second()); - } - return null; + final RecipientRecord localRecipientRecord = localRecords.get(recipientToUpdateId); + if (localRecipientRecord == null) { + return true; + } + + final SignalServiceProfile remoteProfile = recipientProfileAndCredentialPair.second().getProfile(); + final Optional remoteCredential = recipientProfileAndCredentialPair.second().getExpiringProfileKeyCredential(); + + try { + return isUpdated(localRecipientRecord, remoteProfile, remoteCredential); + } catch (InvalidCiphertextException | IOException e) { + Log.w(TAG, "Could not compare new and old profiles.", e); + return true; + } + }) + .toList(); + + Log.d(TAG, "Committing updates to " + updatedProfiles.size() + " of " + operationState.profiles.size() + " retrieved profiles."); + + if (!updatedProfiles.isEmpty()) { + //noinspection SimplifyStreamApiCallChains + ListUtil.chunk(updatedProfiles, 150).stream().forEach(list -> { + SignalDatabase.runInTransaction((db) -> { + for (Pair profile : list) { + process(profile.first(), profile.second()); + } + return null; + }); }); - }); + } recipientTable.markProfilesFetched(success, System.currentTimeMillis()); @@ -336,6 +371,49 @@ public class RetrieveProfileJob extends BaseJob { @Override public void onFailure() {} + private boolean isUpdated(RecipientRecord localRecipientRecord, SignalServiceProfile remoteProfile, Optional remoteExpiringProfileKeyCredential) + throws InvalidCiphertextException, IOException { + + if (!Util.equals(localRecipientRecord.getProfileAvatar(), remoteProfile.getAvatar())) { + return true; + } + + if (!Util.equals(localRecipientRecord.getBadges(), Stream.of(remoteProfile.getBadges()).map(Badges::fromServiceBadge).collect(Collectors.toList()))) { + return true; + } + + if (!Util.equals(localRecipientRecord.getCapabilities().getRawBits(), RecipientTable.maskCapabilitiesToLong(remoteProfile.getCapabilities()))) { + return true; + } + + ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(localRecipientRecord.getProfileKey()); + final UnidentifiedAccessMode accessMode = deriveUnidentifiedAccessMode(profileKey, + remoteProfile.getUnidentifiedAccess(), + remoteProfile.isUnrestrictedUnidentifiedAccess()); + if (!Util.equals(localRecipientRecord.getUnidentifiedAccessMode(), accessMode)) { + return true; + } + + if (profileKey == null) { + return false; + } + + final ProfileName newProfileName = ProfileName.fromSerialized(ProfileUtil.decryptString(profileKey, remoteProfile.getName())); + if (!Util.equals(localRecipientRecord.getProfileName(), newProfileName)) { + return true; + } + + if (!Util.equals(localRecipientRecord.getAbout(), ProfileUtil.decryptString(profileKey, remoteProfile.getAbout()))) { + return true; + } + + if (remoteExpiringProfileKeyCredential.isPresent() && !Util.equals(localRecipientRecord.getExpiringProfileKeyCredential(), remoteExpiringProfileKeyCredential.get())) { + return true; + } + + return false; + } + private void process(Recipient recipient, ProfileAndCredential profileAndCredential) { SignalServiceProfile profile = profileAndCredential.getProfile(); ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); @@ -406,19 +484,28 @@ public class RetrieveProfileJob extends BaseJob { } private void setUnidentifiedAccessMode(Recipient recipient, String unidentifiedAccessVerifier, boolean unrestrictedUnidentifiedAccess) { - RecipientTable recipientTable = SignalDatabase.recipients(); - ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); + + final ProfileKey profileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); + final UnidentifiedAccessMode newMode = deriveUnidentifiedAccessMode(profileKey, unidentifiedAccessVerifier, unrestrictedUnidentifiedAccess); + if (recipient.getUnidentifiedAccessMode() != newMode) { + if (newMode == UnidentifiedAccessMode.UNRESTRICTED) { + Log.i(TAG, "Marking recipient UD status as unrestricted."); + } else if (profileKey == null || unidentifiedAccessVerifier == null) { + Log.i(TAG, "Marking recipient UD status as disabled."); + } else { + Log.i(TAG, "Marking recipient UD status as " + newMode.name() + " after verification."); + } + SignalDatabase.recipients().setUnidentifiedAccessMode(recipient.getId(), newMode); + } + } + + + private UnidentifiedAccessMode deriveUnidentifiedAccessMode(ProfileKey profileKey, String unidentifiedAccessVerifier, boolean unrestrictedUnidentifiedAccess) { if (unrestrictedUnidentifiedAccess && unidentifiedAccessVerifier != null) { - if (recipient.getUnidentifiedAccessMode() != UnidentifiedAccessMode.UNRESTRICTED) { - Log.i(TAG, "Marking recipient UD status as unrestricted."); - recipientTable.setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.UNRESTRICTED); - } + return UnidentifiedAccessMode.UNRESTRICTED; } else if (profileKey == null || unidentifiedAccessVerifier == null) { - if (recipient.getUnidentifiedAccessMode() != UnidentifiedAccessMode.DISABLED) { - Log.i(TAG, "Marking recipient UD status as disabled."); - recipientTable.setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.DISABLED); - } + return UnidentifiedAccessMode.DISABLED; } else { ProfileCipher profileCipher = new ProfileCipher(profileKey); boolean verifiedUnidentifiedAccess; @@ -430,12 +517,7 @@ public class RetrieveProfileJob extends BaseJob { verifiedUnidentifiedAccess = false; } - UnidentifiedAccessMode mode = verifiedUnidentifiedAccess ? UnidentifiedAccessMode.ENABLED : UnidentifiedAccessMode.DISABLED; - - if (recipient.getUnidentifiedAccessMode() != mode) { - Log.i(TAG, "Marking recipient UD status as " + mode.name() + " after verification."); - recipientTable.setUnidentifiedAccessMode(recipient.getId(), mode); - } + return verifiedUnidentifiedAccess ? UnidentifiedAccessMode.ENABLED : UnidentifiedAccessMode.DISABLED; } }