Update RingRTC to 2.65.0

Co-authored-by: emir-signal <emir@signal.org>
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
adel-signal
2026-02-17 14:40:48 -08:00
committed by Alex Hart
parent 2bc571ffd3
commit 6986acd6f4
39 changed files with 85 additions and 172 deletions

View File

@@ -11,7 +11,6 @@ import androidx.lifecycle.Observer;
import com.bumptech.glide.RequestManager;
import org.signal.ringrtc.CallLinkEpoch;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
@@ -30,8 +29,8 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.polls.PollRecord;
import org.thoughtcrime.securesms.polls.PollOption;
import org.thoughtcrime.securesms.polls.PollRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@@ -136,7 +135,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
void onEditedIndicatorClicked(@NonNull ConversationMessage conversationMessage);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey, @Nullable CallLinkEpoch callLinkEpoch);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
void onShowSafetyTips(boolean forGroup);
void onReportSpamLearnMoreClicked();
void onMessageRequestAcceptOptionsClicked();

View File

@@ -40,7 +40,6 @@ class CallLinkArchiveExporter(private val cursor: Cursor) : Iterator<ArchiveReci
id = callLink.recipientId.toLong(),
callLink = CallLink(
rootKey = callLink.credentials!!.linkKeyBytes.toByteString(),
epoch = callLink.credentials.epochBytes?.takeIf { it.size == 4 }?.toByteString(),
adminKey = callLink.credentials.adminPassBytes?.toByteString()?.nullIfEmpty(),
name = callLink.state.name,
expirationMs = expirationTime.takeIf { it != Long.MAX_VALUE }?.clampToValidBackupRange() ?: 0,

View File

@@ -42,11 +42,7 @@ object CallLinkArchiveImporter {
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = CallLinkRoomId.fromCallLinkRootKey(rootKey),
credentials = CallLinkCredentials(
callLink.rootKey.toByteArray(),
callLink.epoch?.toByteArray(),
callLink.adminKey?.toByteArray()
),
credentials = CallLinkCredentials(callLink.rootKey.toByteArray(), callLink.adminKey?.toByteArray()),
state = SignalCallLinkState(
name = callLink.name,
restrictions = callLink.restrictions.toLocal(),

View File

@@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.calls.links
import io.reactivex.rxjava3.core.Observable
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallException
import org.signal.ringrtc.CallLinkEpoch
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.DatabaseObserver
@@ -23,7 +22,6 @@ import java.net.URLDecoder
*/
object CallLinks {
private const val ROOT_KEY = "key"
private const val EPOCH = "epoch"
private const val LEGACY_HTTPS_LINK_PREFIX = "https://signal.link/call#key="
private const val LEGACY_SGNL_LINK_PREFIX = "sgnl://signal.link/call#key="
private const val HTTPS_LINK_PREFIX = "https://signal.link/call/#key="
@@ -31,13 +29,7 @@ object CallLinks {
private val TAG = Log.tag(CallLinks::class.java)
fun url(rootKeyBytes: ByteArray, epochBytes: ByteArray?): String {
return if (epochBytes == null) {
"$HTTPS_LINK_PREFIX${CallLinkRootKey(rootKeyBytes)}"
} else {
"$HTTPS_LINK_PREFIX${CallLinkRootKey(rootKeyBytes)}&epoch=${CallLinkEpoch.fromBytes(epochBytes)}"
}
}
fun url(rootKeyBytes: ByteArray): String = "$HTTPS_LINK_PREFIX${CallLinkRootKey(rootKeyBytes)}"
fun watchCallLink(roomId: CallLinkRoomId): Observable<CallLinkTable.CallLink> {
return Observable.create { emitter ->
@@ -78,13 +70,8 @@ object CallLinks {
return url.split("#").last().startsWith("key=")
}
data class CallLinkParseResult(
val rootKey: CallLinkRootKey,
val epoch: CallLinkEpoch?
)
@JvmStatic
fun parseUrl(url: String): CallLinkParseResult? {
fun parseUrl(url: String): CallLinkRootKey? {
if (!isPrefixedCallLink(url)) {
Log.w(TAG, "Invalid url prefix.")
return null
@@ -132,13 +119,9 @@ object CallLinks {
}
return try {
val epoch = fragmentQuery[EPOCH]?.let { s -> CallLinkEpoch(s) }
CallLinkParseResult(
rootKey = CallLinkRootKey(key),
epoch = epoch
)
return CallLinkRootKey(key)
} catch (e: CallException) {
Log.w(TAG, "Invalid root key or epoch found in fragment query string.")
Log.w(TAG, "Invalid root key found in fragment query string.")
null
}
}

View File

@@ -53,7 +53,7 @@ import org.signal.core.ui.R as CoreUiR
@Composable
private fun SignalCallRowPreview() {
val callLink = remember {
val credentials = CallLinkCredentials(byteArrayOf(1, 2, 3, 4), byteArrayOf(0, 1, 2, 3), byteArrayOf(5, 6, 7, 8))
val credentials = CallLinkCredentials(byteArrayOf(1, 2, 3, 4), byteArrayOf(5, 6, 7, 8))
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 3, 5, 7)),
@@ -97,7 +97,7 @@ fun SignalCallRow(
"https://signal.call.example.com"
} else {
remember(callLink.credentials) {
callLink.credentials?.let { CallLinks.url(it.linkKeyBytes, it.epochBytes) } ?: ""
callLink.credentials?.let { CallLinks.url(it.linkKeyBytes) } ?: ""
}
}

View File

@@ -162,7 +162,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
startActivity(
ShareActivity.sendSimpleText(
requireContext(),
getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes))
getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.linkKeyBytes))
)
)
}
@@ -176,7 +176,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = {
when (it) {
is EnsureCallLinkCreatedResult.Success -> {
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes))
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes))
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
}
@@ -191,7 +191,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
is EnsureCallLinkCreatedResult.Success -> {
val mimeType = Intent.normalizeMimeType("text/plain")
val shareIntent = ShareCompat.IntentBuilder(requireContext())
.setText(CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes))
.setText(CallLinks.url(viewModel.linkKeyBytes))
.setType(mimeType)
.createChooserIntent()

View File

@@ -52,9 +52,6 @@ class CreateCallLinkViewModel(
val linkKeyBytes: ByteArray
get() = callLink.value.credentials!!.linkKeyBytes
val epochBytes: ByteArray?
get() = callLink.value.credentials!!.epochBytes
private val internalShowAlreadyInACall = MutableStateFlow(false)
val showAlreadyInACall: StateFlow<Boolean> = internalShowAlreadyInACall

View File

@@ -119,7 +119,7 @@ class DefaultCallLinkDetailsCallback(
override fun onShareClicked() {
val mimeType = Intent.normalizeMimeType("text/plain")
val shareIntent = ShareCompat.IntentBuilder(activity)
.setText(CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot))
.setText(CallLinks.url(viewModel.rootKeySnapshot))
.setType(mimeType)
.createChooserIntent()
@@ -131,7 +131,7 @@ class DefaultCallLinkDetailsCallback(
}
override fun onCopyClicked() {
Util.copyToClipboard(activity, CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot))
Util.copyToClipboard(activity, CallLinks.url(viewModel.rootKeySnapshot))
Toast.makeText(activity, R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
}
@@ -139,7 +139,7 @@ class DefaultCallLinkDetailsCallback(
activity.startActivity(
ShareActivity.sendSimpleText(
activity,
activity.getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot))
activity.getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.rootKeySnapshot))
)
)
}
@@ -324,7 +324,6 @@ private fun CallLinkDetailsScreenPreview() {
val callLink = remember {
val credentials = CallLinkCredentials(
byteArrayOf(1, 2, 3, 4),
byteArrayOf(0, 1, 2, 3),
byteArrayOf(3, 4, 5, 6)
)
CallLinkTable.CallLink(

View File

@@ -48,9 +48,6 @@ class CallLinkDetailsViewModel(
val rootKeySnapshot: ByteArray
get() = state.value.callLink?.credentials?.linkKeyBytes ?: error("Call link not loaded yet.")
val epochSnapshot: ByteArray?
get() = state.value.callLink?.credentials?.epochBytes
private val recipientSubject = BehaviorSubject.create<Recipient>()
val recipientSnapshot: Recipient?
get() = recipientSubject.value

View File

@@ -172,11 +172,11 @@ public class LinkPreviewView extends FrameLayout {
spinner.setVisibility(GONE);
noPreview.setVisibility(GONE);
CallLinks.CallLinkParseResult callLinkParseResult = CallLinks.isCallLink(linkPreview.getUrl()) ? CallLinks.parseUrl(linkPreview.getUrl()) : null;
CallLinkRootKey callLinkRootKey = CallLinks.isCallLink(linkPreview.getUrl()) ? CallLinks.parseUrl(linkPreview.getUrl()) : null;
if (!Util.isEmpty(linkPreview.getTitle())) {
title.setText(linkPreview.getTitle());
title.setVisibility(VISIBLE);
} else if (callLinkParseResult != null) {
} else if (callLinkRootKey != null) {
title.setText(R.string.Recipient_signal_call);
title.setVisibility(VISIBLE);
} else {
@@ -186,7 +186,7 @@ public class LinkPreviewView extends FrameLayout {
if (showDescription && !Util.isEmpty(linkPreview.getDescription())) {
description.setText(linkPreview.getDescription());
description.setVisibility(VISIBLE);
} else if (callLinkParseResult != null) {
} else if (callLinkRootKey != null) {
description.setText(R.string.LinkPreviewView__use_this_link_to_join_a_signal_call);
description.setVisibility(VISIBLE);
} else {
@@ -221,14 +221,14 @@ public class LinkPreviewView extends FrameLayout {
thumbnail.get().setImageResource(requestManager, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false);
thumbnail.get().showSecondaryText(false);
thumbnail.get().setOutlineEnabled(true);
} else if (callLinkParseResult != null) {
} else if (callLinkRootKey != null) {
thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail);
thumbnail.get().setImageDrawable(
requestManager,
new FallbackAvatarDrawable(
getContext(),
new FallbackAvatar.Resource.CallLink(AvatarColorHash.forCallLink(callLinkParseResult.getRootKey().getKeyBytes()))
new FallbackAvatar.Resource.CallLink(AvatarColorHash.forCallLink(callLinkRootKey.getKeyBytes()))
).circleCrop()
);
thumbnail.get().showSecondaryText(false);
@@ -272,7 +272,7 @@ public class LinkPreviewView extends FrameLayout {
thumbnailState.applyState(thumbnail);
}
private @StringRes static int getLinkPreviewErrorString(@Nullable LinkPreviewRepository.Error customError) {
private @StringRes static int getLinkPreviewErrorString(@Nullable LinkPreviewRepository.Error customError) {
return customError == LinkPreviewRepository.Error.GROUP_LINK_INACTIVE ? R.string.LinkPreviewView_this_group_link_is_not_active
: R.string.LinkPreviewView_no_link_preview_available;
}

View File

@@ -34,8 +34,6 @@ class ControlsAndInfoViewModel(
val rootKeySnapshot: ByteArray
get() = state.value.callLink?.credentials?.linkKeyBytes ?: error("Call link not loaded yet.")
val epochSnapshot: ByteArray?
get() = state.value.callLink?.credentials?.epochBytes
fun setRecipient(recipient: Recipient) {
if (recipient.isCallLink && callRecipientId != recipient.id) {

View File

@@ -31,7 +31,7 @@ class CallInfoCallbacks(
override fun onShareLinkClicked() {
val mimeType = Intent.normalizeMimeType("text/plain")
val shareIntent = ShareCompat.IntentBuilder(activity)
.setText(CallLinks.url(controlsAndInfoViewModel.rootKeySnapshot, controlsAndInfoViewModel.epochSnapshot))
.setText(CallLinks.url(controlsAndInfoViewModel.rootKeySnapshot))
.setType(mimeType)
.createChooserIntent()

View File

@@ -74,6 +74,7 @@ import org.signal.core.util.DimensionUnit;
import org.signal.core.util.StringUtil;
import org.signal.core.util.logging.Log;
import org.signal.core.ui.view.Stub;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
@@ -1234,14 +1235,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
//noinspection ConstantConditions
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
CallLinks.CallLinkParseResult callLinkParseResult = CallLinks.isCallLink(linkPreview.getUrl()) ? CallLinks.parseUrl(linkPreview.getUrl()) : null;
if (callLinkParseResult != null) {
CallLinkRootKey callLinkRootKey = CallLinks.isCallLink(linkPreview.getUrl()) ? CallLinks.parseUrl(linkPreview.getUrl()) : null;
if (callLinkRootKey != null) {
joinCallLinkStub.setVisibility(View.VISIBLE);
joinCallLinkStub.get().setTextColor(ContextCompat.getColor(context, messageRecord.isOutgoing() ? org.signal.core.ui.R.color.signal_light_colorOnPrimary : org.signal.core.ui.R.color.signal_colorOnPrimaryContainer));
joinCallLinkStub.get().setBackgroundColor(ContextCompat.getColor(context, messageRecord.isOutgoing() ? org.signal.core.ui.R.color.signal_light_colorTransparent2 : org.signal.core.ui.R.color.signal_colorOnPrimary));
joinCallLinkStub.get().setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onJoinCallLink(callLinkParseResult.getRootKey(), callLinkParseResult.getEpoch());
eventListener.onJoinCallLink(callLinkRootKey);
}
});
}

View File

@@ -121,7 +121,6 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.core.util.setActionItemTint
import org.signal.donations.InAppPaymentType
import org.signal.ringrtc.CallLinkEpoch
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.BlockUnblockDialog
import org.thoughtcrime.securesms.GroupMembersDialog
@@ -3740,8 +3739,8 @@ class ConversationFragment :
GroupDescriptionDialog.show(childFragmentManager, groupName, description, shouldLinkifyWebLinks)
}
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) {
CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey, callLinkEpoch) {
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey) {
YouAreAlreadyInACallSnackbar.show(requireView())
}
}

View File

@@ -21,21 +21,18 @@ import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.ringrtc.CallLinkEpoch
import org.signal.ringrtc.CallLinkRootKey
import org.signal.ringrtc.CallLinkState.Restrictions
import org.thoughtcrime.securesms.calls.log.CallLogRow
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.CallLinkUpdateSendJob
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.internal.push.SyncMessage
import java.time.Instant
import java.time.temporal.ChronoUnit
@@ -50,7 +47,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
const val TABLE_NAME = "call_link"
const val ID = "_id"
const val ROOT_KEY = "root_key"
const val EPOCH = "epoch"
const val ROOM_ID = "room_id"
const val ADMIN_KEY = "admin_key"
const val NAME = "name"
@@ -72,8 +68,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
$REVOKED INTEGER NOT NULL,
$EXPIRATION INTEGER NOT NULL,
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
$DELETION_TIMESTAMP INTEGER DEFAULT 0 NOT NULL,
$EPOCH BLOB DEFAULT NULL
$DELETION_TIMESTAMP INTEGER DEFAULT 0 NOT NULL
)
"""
@@ -133,7 +128,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
.values(
contentValuesOf(
ROOT_KEY to credentials.linkKeyBytes,
EPOCH to credentials.epochBytes,
ADMIN_KEY to credentials.adminPassBytes
)
)
@@ -193,10 +187,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
.readToSingleObject { CallLinkDeserializer.deserialize(it) }
}
fun getOrCreateCallLinkByRootKey(
callLinkRootKey: CallLinkRootKey,
callLinkEpoch: CallLinkEpoch?
): CallLink {
fun getOrCreateCallLinkByRootKey(callLinkRootKey: CallLinkRootKey): CallLink {
val roomId = CallLinkRoomId.fromBytes(callLinkRootKey.deriveRoomId())
val callLink = getCallLinkByRoomId(roomId)
return if (callLink == null) {
@@ -205,7 +196,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
roomId = roomId,
credentials = CallLinkCredentials(
linkKeyBytes = callLinkRootKey.keyBytes,
epochBytes = callLinkEpoch?.bytes,
adminPassBytes = null
),
state = SignalCallLinkState(),
@@ -215,33 +205,12 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
insertCallLink(link)
return getCallLinkByRoomId(roomId)!!
} else {
if (callLink.credentials?.epoch != callLinkEpoch) {
val modifiedCallLink = overwriteEpoch(callLink, callLinkEpoch)
AppDependencies.jobManager.add(
CallLinkUpdateSendJob(
callLink.credentials!!.roomId,
SyncMessage.CallLinkUpdate.Type.UPDATE
)
)
modifiedCallLink
} else {
callLink
}
callLink
}
}
private fun overwriteEpoch(callLink: CallLink, callLinkEpoch: CallLinkEpoch?): CallLink {
val modifiedCallLink = callLink.copy(
deletionTimestamp = 0,
credentials = callLink.credentials!!.copy(epochBytes = callLinkEpoch?.bytes)
)
updateCallLinkCredentials(modifiedCallLink.roomId, modifiedCallLink.credentials!!)
return modifiedCallLink
}
fun insertOrUpdateCallLinkByRootKey(
callLinkRootKey: CallLinkRootKey,
callLinkEpoch: CallLinkEpoch?,
adminPassKey: ByteArray?,
deletionTimestamp: Long = 0L,
storageId: StorageId? = null
@@ -256,7 +225,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
roomId = roomId,
credentials = CallLinkCredentials(
linkKeyBytes = callLinkRootKey.keyBytes,
epochBytes = callLinkEpoch?.bytes,
adminPassBytes = adminPassKey
),
state = SignalCallLinkState(),
@@ -283,8 +251,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
writableDatabase.update(TABLE_NAME)
.values(
ADMIN_KEY to adminPassKey,
ROOT_KEY to callLinkRootKey.keyBytes,
EPOCH to callLinkEpoch?.bytes
ROOT_KEY to callLinkRootKey.keyBytes
)
.where("$ROOM_ID = ?", callLink.roomId.serialize())
.run()
@@ -495,7 +462,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
RECIPIENT_ID to data.recipientId.takeIf { it != RecipientId.UNKNOWN }?.toLong(),
ROOM_ID to data.roomId.serialize(),
ROOT_KEY to data.credentials?.linkKeyBytes,
EPOCH to data.credentials?.epochBytes,
ADMIN_KEY to data.credentials?.adminPassBytes
).apply {
putAll(data.state.serialize())
@@ -519,7 +485,6 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
credentials = data.requireBlob(ROOT_KEY)?.let { linkKey ->
CallLinkCredentials(
linkKeyBytes = linkKey,
epochBytes = data.requireBlob(EPOCH),
adminPassBytes = data.requireBlob(ADMIN_KEY)
)
},

View File

@@ -154,6 +154,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V297_AddPinnedMessa
import org.thoughtcrime.securesms.database.helpers.migration.V298_DoNotBackupReleaseNotes
import org.thoughtcrime.securesms.database.helpers.migration.V299_AddAttachmentMetadataTable
import org.thoughtcrime.securesms.database.helpers.migration.V300_AddKeyTransparencyColumn
import org.thoughtcrime.securesms.database.helpers.migration.V301_RemoveCallLinkEpoch
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -314,10 +315,11 @@ object SignalDatabaseMigrations {
297 to V297_AddPinnedMessageColumns,
298 to V298_DoNotBackupReleaseNotes,
299 to V299_AddAttachmentMetadataTable,
300 to V300_AddKeyTransparencyColumn
300 to V300_AddKeyTransparencyColumn,
301 to V301_RemoveCallLinkEpoch
)
const val DATABASE_VERSION = 300
const val DATABASE_VERSION = 301
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.thoughtcrime.securesms.database.SQLiteDatabase
/**
* We planned to introduce CallLink epochs as a first-class field to clients.
* Now, we plan to introduce epochs as an internal detail in CallLink root keys.
* Epochs were never enabled in production so no clients should have them.
*/
object V301_RemoveCallLinkEpoch : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE call_link DROP COLUMN epoch")
}
}

View File

@@ -71,7 +71,6 @@ class CallLinkUpdateSendJob private constructor(
val callLinkUpdate = CallLinkUpdate(
rootKey = callLink.credentials.linkKeyBytes.toByteString(),
adminPasskey = callLink.credentials.adminPassBytes?.toByteString(),
epoch = callLink.credentials.epochBytes?.toByteString(),
type = callLinkUpdateType
)

View File

@@ -48,7 +48,6 @@ class RefreshCallLinkDetailsJob private constructor(
val manager: SignalCallLinkManager = AppDependencies.signalCallManager.callLinkManager
val credentials = CallLinkCredentials(
linkKeyBytes = callLinkUpdate.rootKey!!.toByteArray(),
epochBytes = callLinkUpdate.epoch?.toByteArray(),
adminPassBytes = callLinkUpdate.adminPasskey?.toByteArray()
)

View File

@@ -19,7 +19,6 @@ import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidMessageException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.ringrtc.CallLinkEpoch;
import org.signal.ringrtc.CallLinkRootKey;
import org.signal.storageservice.storage.protos.groups.local.DecryptedGroupJoinInfo;
import org.thoughtcrime.securesms.R;
@@ -328,20 +327,15 @@ public class LinkPreviewRepository {
@NonNull String callLinkUrl,
@NonNull Callback callback) {
CallLinks.CallLinkParseResult linkParseResult = CallLinks.parseUrl(callLinkUrl);
if (linkParseResult == null) {
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(callLinkUrl);
if (callLinkRootKey == null) {
callback.onError(Error.PREVIEW_NOT_AVAILABLE);
return () -> { };
}
CallLinkEpoch epoch = linkParseResult.getEpoch();
byte[] epochBytes = epoch != null ? epoch.getBytes() : null;
Disposable disposable = AppDependencies.getSignalCallManager()
.getCallLinkManager()
.readCallLink(new CallLinkCredentials(linkParseResult.getRootKey().getKeyBytes(),
epochBytes,
null))
.readCallLink(new CallLinkCredentials(callLinkRootKey.getKeyBytes(), null))
.observeOn(Schedulers.io())
.subscribe(
result -> {

View File

@@ -13,7 +13,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallLinkEpoch
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FullScreenDialogFragment
@@ -362,7 +361,7 @@ class MessageDetailsFragment : FullScreenDialogFragment(), MessageDetailsAdapter
Log.w(TAG, "Not yet implemented!", Exception())
}
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) {
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
Log.w(TAG, "Not yet implemented!", Exception())
}

View File

@@ -1391,7 +1391,6 @@ object SyncMessageProcessor {
roomId,
CallLinkCredentials(
callLinkUpdate.rootKey!!.toByteArray(),
callLinkUpdate.epoch?.toByteArray(),
callLinkUpdate.adminPasskey?.toByteArray()
)
)
@@ -1403,7 +1402,6 @@ object SyncMessageProcessor {
roomId = roomId,
credentials = CallLinkCredentials(
linkKeyBytes = callLinkRootKey.keyBytes,
epochBytes = callLinkUpdate.epoch?.toByteArray(),
adminPassBytes = callLinkUpdate.adminPasskey?.toByteArray()
),
state = SignalCallLinkState(),

View File

@@ -70,7 +70,6 @@ class CallLinkPreJoinActionProcessor(
serverPublicParams.endorsementPublicKey,
callLinkAuthCredentialPresentation.serialize(),
callLinkRootKey,
callLink.credentials.epoch,
callLink.credentials.adminPassBytes,
ByteArray(0),
AUDIO_LEVELS_INTERVAL,

View File

@@ -24,7 +24,6 @@ import org.signal.libsignal.zkgroup.calllinks.CallLinkSecretParams;
import org.signal.libsignal.zkgroup.groups.GroupIdentifier;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.CallId;
import org.signal.ringrtc.CallLinkEpoch;
import org.signal.ringrtc.CallLinkRootKey;
import org.signal.ringrtc.CallManager;
import org.signal.ringrtc.CallSummary;
@@ -420,7 +419,6 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
}
CallLinkRootKey callLinkRootKey = new CallLinkRootKey(callLink.getCredentials().getLinkKeyBytes());
CallLinkEpoch callLinkEpoch = callLink.getCredentials().getEpoch();
GenericServerPublicParams genericServerPublicParams = new GenericServerPublicParams(AppDependencies.getSignalServiceNetworkAccess()
.getConfiguration()
.getGenericServerPublicParams());
@@ -432,7 +430,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
CallLinkSecretParams.deriveFromRootKey(callLinkRootKey.getKeyBytes())
);
callManager.peekCallLinkCall(SignalStore.internal().getGroupCallingServer(), callLinkAuthCredentialPresentation.serialize(), callLinkRootKey, callLinkEpoch, peekInfo -> {
callManager.peekCallLinkCall(SignalStore.internal().getGroupCallingServer(), callLinkAuthCredentialPresentation.serialize(), callLinkRootKey, peekInfo -> {
PeekInfo info = peekInfo.getValue();
if (info == null) {
Log.w(TAG, "Failed to get peek info: " + peekInfo.getStatus());

View File

@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.service.webrtc.links
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.signal.ringrtc.CallLinkEpoch
import org.signal.ringrtc.CallLinkRootKey
/**
@@ -16,7 +15,6 @@ import org.signal.ringrtc.CallLinkRootKey
@Parcelize
data class CallLinkCredentials(
val linkKeyBytes: ByteArray,
val epochBytes: ByteArray?,
val adminPassBytes: ByteArray?
) : Parcelable {
@@ -24,10 +22,6 @@ data class CallLinkCredentials(
CallLinkRoomId.fromCallLinkRootKey(CallLinkRootKey(linkKeyBytes))
}
val epoch: CallLinkEpoch? by lazy {
epochBytes?.let { CallLinkEpoch.fromBytes(it) }
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -41,12 +35,6 @@ data class CallLinkCredentials(
} else if (other.adminPassBytes != null) {
return false
}
if (epochBytes != null) {
if (other.epochBytes == null) return false
if (!epochBytes.contentEquals(other.epochBytes)) return false
} else if (other.epochBytes != null) {
return false
}
return true
}
@@ -64,7 +52,6 @@ data class CallLinkCredentials(
fun generate(): CallLinkCredentials {
return CallLinkCredentials(
CallLinkRootKey.generate().keyBytes,
null,
CallLinkRootKey.generateAdminPasskey()
)
}

View File

@@ -120,10 +120,10 @@ class SignalCallLinkManager(
) { result ->
if (result.isSuccess) {
Log.d(TAG, "Successfully created call link.")
val epoch = result.value!!.epoch
val rootKey = result.value!!.rootKey
emitter.onSuccess(
CreateCallLinkResult.Success(
credentials = CallLinkCredentials(rootKey.keyBytes, epoch?.bytes, adminPassKey),
credentials = CallLinkCredentials(rootKey.keyBytes, adminPassKey),
state = result.value!!.toAppState()
)
)
@@ -142,8 +142,7 @@ class SignalCallLinkManager(
callManager.readCallLink(
SignalStore.internal.groupCallingServer,
requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes).serialize(),
CallLinkRootKey(credentials.linkKeyBytes),
credentials.epoch
CallLinkRootKey(credentials.linkKeyBytes)
) {
if (it.isSuccess) {
emitter.onSuccess(ReadCallLinkResult.Success(it.value!!.toAppState()))
@@ -170,7 +169,6 @@ class SignalCallLinkManager(
SignalStore.internal.groupCallingServer,
credentialPresentation.serialize(),
CallLinkRootKey(credentials.linkKeyBytes),
credentials.epoch,
credentials.adminPassBytes,
name
) { result ->
@@ -198,7 +196,6 @@ class SignalCallLinkManager(
SignalStore.internal.groupCallingServer,
credentialPresentation.serialize(),
CallLinkRootKey(credentials.linkKeyBytes),
credentials.epoch,
credentials.adminPassBytes,
restrictions
) { result ->
@@ -225,7 +222,6 @@ class SignalCallLinkManager(
SignalStore.internal.groupCallingServer,
credentialPresentation.serialize(),
CallLinkRootKey(credentials.linkKeyBytes),
credentials.epoch,
credentials.adminPassBytes
) { result ->
if (result.isSuccess && result.value == true) {

View File

@@ -9,7 +9,6 @@ import okio.ByteString.Companion.toByteString
import org.signal.core.util.isNotEmpty
import org.signal.core.util.logging.Log
import org.signal.core.util.toOptional
import org.signal.ringrtc.CallLinkEpoch
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
@@ -49,7 +48,6 @@ class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCallLinkReco
if (callLink != null && callLink.credentials?.adminPassBytes != null) {
return SignalCallLinkRecord.newBuilder(null).apply {
rootKey = callRootKey.keyBytes.toByteString()
epoch = callLink.credentials.epochBytes?.toByteString()
adminPasskey = callLink.credentials.adminPassBytes.toByteString()
deletedAtTimestampMs = callLink.deletionTimestamp
}.build().toSignalCallLinkRecord(StorageId.forCallLink(keyGenerator.generate())).toOptional()
@@ -90,11 +88,8 @@ class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCallLinkReco
private fun insertOrUpdateRecord(record: SignalCallLinkRecord) {
val rootKey = CallLinkRootKey(record.proto.rootKey.toByteArray())
val epoch = record.proto.epoch?.let { CallLinkEpoch.fromBytes(it.toByteArray()) }
SignalDatabase.callLinks.insertOrUpdateCallLinkByRootKey(
callLinkRootKey = rootKey,
callLinkEpoch = epoch,
adminPassKey = record.proto.adminPasskey.toByteArray(),
deletionTimestamp = record.proto.deletedAtTimestampMs,
storageId = record.id

View File

@@ -274,7 +274,6 @@ object StorageSyncModels {
return SignalCallLinkRecord.newBuilder(null).apply {
rootKey = callLink.credentials.linkKeyBytes.toByteString()
epoch = callLink.credentials.epochBytes?.toByteString()
adminPasskey = adminPassword.toByteString()
deletedAtTimestampMs = deletedTimestamp
}.build().toSignalCallLinkRecord(StorageId.forCallLink(rawStorageId))

View File

@@ -28,7 +28,6 @@ import org.signal.core.util.concurrent.JvmRxExtensions;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallLinkEpoch;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.calls.links.CallLinks;
@@ -334,8 +333,8 @@ public class CommunicationActions {
return;
}
CallLinks.CallLinkParseResult linkParseResult = CallLinks.parseUrl(potentialUrl);
if (linkParseResult == null) {
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(potentialUrl);
if (callLinkRootKey == null) {
Log.w(TAG, "Failed to parse root key from call link");
new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.CommunicationActions_invalid_link)
@@ -345,7 +344,7 @@ public class CommunicationActions {
return;
}
startVideoCall(new ActivityCallContext(activity), linkParseResult.getRootKey(), linkParseResult.getEpoch(), onUserAlreadyInAnotherCall);
startVideoCall(new ActivityCallContext(activity), callLinkRootKey, onUserAlreadyInAnotherCall);
}
/**
@@ -374,14 +373,14 @@ public class CommunicationActions {
*
* @param fragment The fragment, which will be used for context and permissions routing.
*/
public static void startVideoCall(@NonNull Fragment fragment, @NonNull CallLinkRootKey rootKey, @Nullable CallLinkEpoch epoch, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
startVideoCall(new FragmentCallContext(fragment), rootKey, epoch, onUserAlreadyInAnotherCall);
public static void startVideoCall(@NonNull Fragment fragment, @NonNull CallLinkRootKey rootKey, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
startVideoCall(new FragmentCallContext(fragment), rootKey, onUserAlreadyInAnotherCall);
}
private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey, @Nullable CallLinkEpoch epoch, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
SimpleTask.run(() -> {
CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId());
CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey, epoch);
CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey);
if (callLink.getState().hasBeenRevoked()) {
return Optional.<Recipient>empty();