mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 18:26:57 +00:00
Add support for rendering session switchover events.
This commit is contained in:
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user