Add support for rendering session switchover events.

This commit is contained in:
Greyson Parrelli
2023-02-07 11:26:57 -05:00
parent 03c68375db
commit 9e056e5dd0
11 changed files with 229 additions and 878 deletions

View File

@@ -9,6 +9,8 @@ import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.Hex
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.isAbsent
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
@@ -220,7 +222,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
sectionHeaderPref(DSLSettingsText.from("PNP"))
clickPref(
title = DSLSettingsText.from("Split contact"),
title = DSLSettingsText.from("Split and create threads"),
summary = DSLSettingsText.from("Splits this contact into two recipients and two threads so that you can test merging them together. This will remain the 'primary' recipient."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
@@ -257,6 +259,41 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
.show()
}
)
clickPref(
title = DSLSettingsText.from("Split without creating threads"),
summary = DSLSettingsText.from("Splits this contact into two recipients so you can test merging them together. This will become the PNI-based recipient. Another recipient will be made with this ACI and profile key. Doing a CDS refresh should allow you to see a Session Switchover Event, as long as you had a session with this PNI."),
isEnabled = FeatureFlags.phoneNumberPrivacy(),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
if (recipient.pni.isAbsent()) {
Toast.makeText(context, "Recipient doesn't have a PNI! Can't split.", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
if (recipient.serviceId.isAbsent()) {
Toast.makeText(context, "Recipient doesn't have a serviceId! Can't split.", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
SignalDatabase.recipients.debugRemoveAci(recipient.id)
val aciRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.requireServiceId(), null, null)
recipient.profileKey?.let { profileKey ->
SignalDatabase.recipients.setProfileKey(aciRecipientId, ProfileKey(profileKey))
}
SignalDatabase.recipients.debugClearProfileData(recipient.id)
Toast.makeText(context, "Done! Split the ACI and profile key off into $aciRecipientId", Toast.LENGTH_SHORT).show()
}
.show()
}
)
}
}

View File

@@ -22,7 +22,6 @@ import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
@@ -73,6 +72,7 @@ import org.thoughtcrime.securesms.database.model.UpdateDescription;
import org.thoughtcrime.securesms.glide.GlideLiveDataTarget;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -85,7 +85,6 @@ import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@@ -637,6 +636,14 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
return emphasisAdded(context, "", defaultTint);
} else if (MessageTypes.isBadDecryptType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_delivery_issue), defaultTint);
} else if (MessageTypes.isThreadMergeType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_history_has_been_merged), defaultTint);
} else if (MessageTypes.isSessionSwitchoverType(thread.getType())) {
if (thread.getRecipient().getE164().isPresent()) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_s_belongs_to_s, PhoneNumberFormatter.prettyPrint(thread.getRecipient().requireE164()), thread.getRecipient().getDisplayName(context)), defaultTint);
} else {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_safety_number_changed), defaultTint);
}
} else {
ThreadTable.Extra extra = thread.getExtra();
if (extra != null && extra.isViewOnce()) {

View File

@@ -2406,6 +2406,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
@VisibleForTesting
fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, inputPni: PNI?): RecipientId {
var hadThreadMerge = false
for (operation in changeSet.operations) {
@Exhaustive
when (operation) {
@@ -2465,16 +2466,21 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
.run()
}
is PnpOperation.Merge -> {
merge(operation.primaryId, operation.secondaryId, inputPni)
val mergeResult: MergeResult = merge(operation.primaryId, operation.secondaryId, inputPni)
hadThreadMerge = hadThreadMerge || mergeResult.neededThreadMerge
}
is PnpOperation.SessionSwitchoverInsert -> {
val threadId: Long? = threads.getThreadIdFor(operation.recipientId)
if (threadId != null) {
val event = SessionSwitchoverEvent
.newBuilder()
.setE164(operation.e164 ?: "")
.build()
SignalDatabase.messages.insertSessionSwitchoverEvent(operation.recipientId, threadId, event)
if (hadThreadMerge) {
Log.d(TAG, "Skipping SSE insert because we already had a thread merge event.")
} else {
val threadId: Long? = threads.getThreadIdFor(operation.recipientId)
if (threadId != null) {
val event = SessionSwitchoverEvent
.newBuilder()
.setE164(operation.e164 ?: "")
.build()
SignalDatabase.messages.insertSessionSwitchoverEvent(operation.recipientId, threadId, event)
}
}
}
is PnpOperation.ChangeNumberInsert -> {
@@ -2622,6 +2628,14 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
)
}
if (!pniVerified && fullData.pniSidRecord != null && finalData.aciSidRecord != null && sessions.hasAnySessionFor(fullData.pniSidRecord.serviceId.toString())) {
breadCrumbs += "FinalUpdateSSE"
operations += PnpOperation.SessionSwitchoverInsert(
recipientId = primaryId,
e164 = finalData.e164
)
}
return PnpChangeSet(
id = PnpIdResolver.PnpNoopId(primaryId),
operations = operations,
@@ -3644,7 +3658,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
* Merges one ACI recipient with an E164 recipient. It is assumed that the E164 recipient does
* *not* have an ACI.
*/
private fun merge(primaryId: RecipientId, secondaryId: RecipientId, newPni: PNI? = null): RecipientId {
private fun merge(primaryId: RecipientId, secondaryId: RecipientId, newPni: PNI? = null): MergeResult {
ensureInTransaction()
val db = writableDatabase
val primaryRecord = getRecord(primaryId)
@@ -3656,7 +3670,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
// Threads
val threadMerge = threads.merge(primaryId, secondaryId)
val threadMerge: ThreadTable.MergeResult = threads.merge(primaryId, secondaryId)
threads.setLastScrolled(threadMerge.threadId, 0)
threads.update(threadMerge.threadId, false, false)
@@ -3721,7 +3735,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(primaryId))
return primaryId
return MergeResult(
finalId = primaryId,
neededThreadMerge = threadMerge.neededMerge
)
}
private fun ensureInTransaction() {
@@ -3845,6 +3863,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
* get them back through CDS).
*/
fun debugClearServiceIds(recipientId: RecipientId? = null) {
check(FeatureFlags.internalUser())
writableDatabase
.update(TABLE_NAME)
.values(
@@ -3868,6 +3888,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
* Should only be used for debugging! A very destructive action that clears all known profile keys and credentials.
*/
fun debugClearProfileData(recipientId: RecipientId? = null) {
check(FeatureFlags.internalUser())
writableDatabase
.update(TABLE_NAME)
.values(
@@ -3896,6 +3918,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
* Should only be used for debugging! Clears the E164 and PNI from a recipient.
*/
fun debugClearE164AndPni(recipientId: RecipientId) {
check(FeatureFlags.internalUser())
writableDatabase
.update(TABLE_NAME)
.values(
@@ -3905,7 +3929,28 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
.where(ID_WHERE, recipientId)
.run()
Recipient.live(recipientId).refresh()
ApplicationDependencies.getRecipientCache().clear()
RecipientId.clearCache()
}
/**
* Should only be used for debugging! Clears the ACI from a contact.
* Only works if the recipient has a PNI.
*/
fun debugRemoveAci(recipientId: RecipientId) {
check(FeatureFlags.internalUser())
writableDatabase.execSQL(
"""
UPDATE $TABLE_NAME
SET $SERVICE_ID = $PNI_COLUMN
WHERE $ID = ? AND $PNI_COLUMN NOT NULL
""".toSingleLine(),
SqlUtil.buildArgs(recipientId)
)
ApplicationDependencies.getRecipientCache().clear()
RecipientId.clearCache()
}
fun getRecord(context: Context, cursor: Cursor): RecipientRecord {
@@ -4131,6 +4176,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
)
}
private data class MergeResult(
val finalId: RecipientId,
val neededThreadMerge: Boolean
)
inner class BulkOperationsHandle internal constructor(private val database: SQLiteDatabase) {
private val pendingRecipients: MutableSet<RecipientId> = mutableSetOf()

View File

@@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent;
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
@@ -235,6 +236,18 @@ public abstract class MessageRecord extends DisplayRecord {
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
} else if (isSessionSwitchoverEventType()) {
try {
SessionSwitchoverEvent event = SessionSwitchoverEvent.parseFrom(Base64.decodeOrThrow(getBody()));
if (event.getE164().isEmpty()) {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)), R.drawable.ic_update_safety_number_16);
} else {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_belongs_to_s, PhoneNumberFormatter.prettyPrint(r.requireE164()), r.getDisplayName(context)), R.drawable.ic_update_info_16);
}
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
} else if (isSmsExportType()) {
int messageResource = SignalStore.misc().getSmsExportPhase().isSmsSupported() ? R.string.MessageRecord__you_will_no_longer_be_able_to_send_sms_messages_from_signal_soon
: R.string.MessageRecord__you_can_no_longer_send_sms_messages_in_signal;
@@ -551,6 +564,10 @@ public abstract class MessageRecord extends DisplayRecord {
return MessageTypes.isThreadMergeType(type);
}
public boolean isSessionSwitchoverEventType() {
return MessageTypes.isSessionSwitchoverType(type);
}
public boolean isSmsExportType() {
return MessageTypes.isSmsExport(type);
}
@@ -575,7 +592,7 @@ public abstract class MessageRecord extends DisplayRecord {
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() ||
isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType() ||
isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType() || isSessionSwitchoverEventType() ||
isPaymentsRequestToActivate() || isPaymentsActivated();
}

View File

@@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.messages.MessageDecryptionUtil;
import org.thoughtcrime.securesms.messages.MessageDecryptionUtil.DecryptionResult;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.FeatureFlags;
@@ -33,7 +32,6 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage;
import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.LinkedList;
@@ -114,10 +112,6 @@ public final class PushDecryptMessageJob extends BaseJob {
if (result.getContent().getSenderKeyDistributionMessage().isPresent()) {
handleSenderKeyDistributionMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getSenderKeyDistributionMessage().get());
}
result.getContent();
if (envelope.hasReportingToken()) {
SignalDatabase.recipients().setReportingToken(RecipientId.from(result.getContent().getSender()), envelope.getReportingToken());
}
if (FeatureFlags.phoneNumberPrivacy() && result.getContent().getPniSignatureMessage().isPresent()) {
handlePniSignatureMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getPniSignatureMessage().get());
@@ -125,6 +119,10 @@ public final class PushDecryptMessageJob extends BaseJob {
Log.w(TAG, "Ignoring PNI signature because the feature flag is disabled!");
}
if (envelope.hasReportingToken()) {
SignalDatabase.recipients().setReportingToken(RecipientId.from(result.getContent().getSender()), envelope.getReportingToken());
}
jobs.add(new PushProcessMessageJob(result.getContent(), smsMessageId, envelope.getTimestamp()));
} else if (result.getException() != null && result.getState() != MessageState.NOOP) {
jobs.add(new PushProcessMessageJob(result.getState(), result.getException(), smsMessageId, envelope.getTimestamp()));

View File

@@ -1042,7 +1042,11 @@ public class Recipient {
}
public @NonNull UnidentifiedAccessMode getUnidentifiedAccessMode() {
return unidentifiedAccessMode;
if (getPni().isPresent() && getPni().equals(getServiceId())) {
return UnidentifiedAccessMode.DISABLED;
} else {
return unidentifiedAccessMode;
}
}
public @Nullable ChatWallpaper getWallpaper() {