mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Add support for call link epochs.
This commit is contained in:
@@ -18,6 +18,7 @@ import com.bumptech.glide.RequestManager
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.ringrtc.CallLinkEpoch
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
@@ -326,7 +327,7 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
|
||||
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) = Unit
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) = Unit
|
||||
|
||||
override fun onItemClick(item: MultiselectPart?) = Unit
|
||||
|
||||
|
||||
@@ -81,7 +81,8 @@ class CallLinkTableTest {
|
||||
roomId = CallLinkRoomId.fromBytes(roomId),
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = roomId,
|
||||
adminPassBytes = null
|
||||
adminPassBytes = null,
|
||||
epochBytes = null
|
||||
),
|
||||
state = SignalCallLinkState(),
|
||||
deletionTimestamp = 0L
|
||||
|
||||
@@ -18,6 +18,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
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.ViewBinderDelegate
|
||||
@@ -292,7 +293,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -133,7 +134,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);
|
||||
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey, @Nullable CallLinkEpoch callLinkEpoch);
|
||||
void onShowSafetyTips(boolean forGroup);
|
||||
void onReportSpamLearnMoreClicked();
|
||||
void onMessageRequestAcceptOptionsClicked();
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.database
|
||||
|
||||
import android.database.Cursor
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.nullIfEmpty
|
||||
import org.signal.ringrtc.CallLinkState
|
||||
@@ -40,6 +41,7 @@ class CallLinkArchiveExporter(private val cursor: Cursor) : Iterator<ArchiveReci
|
||||
id = callLink.recipientId.toLong(),
|
||||
callLink = CallLink(
|
||||
rootKey = callLink.credentials!!.linkKeyBytes.toByteString(),
|
||||
epoch = callLink.credentials.epochBytes?.toByteString() ?: ByteString.EMPTY,
|
||||
adminKey = callLink.credentials.adminPassBytes?.toByteString()?.nullIfEmpty(),
|
||||
name = callLink.state.name,
|
||||
expirationMs = expirationTime.takeIf { it != Long.MAX_VALUE }?.clampToValidBackupRange() ?: 0,
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.importer
|
||||
|
||||
import org.signal.core.util.isEmpty
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.nullIfEmpty
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.signal.ringrtc.CallLinkState
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveCallLink
|
||||
@@ -42,7 +43,11 @@ object CallLinkArchiveImporter {
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = CallLinkRoomId.fromCallLinkRootKey(rootKey),
|
||||
credentials = CallLinkCredentials(callLink.rootKey.toByteArray(), callLink.adminKey?.toByteArray()),
|
||||
credentials = CallLinkCredentials(
|
||||
callLink.rootKey.toByteArray(),
|
||||
callLink.epoch.nullIfEmpty()?.toByteArray(),
|
||||
callLink.adminKey?.toByteArray()
|
||||
),
|
||||
state = SignalCallLinkState(
|
||||
name = callLink.name,
|
||||
restrictions = callLink.restrictions.toLocal(),
|
||||
|
||||
@@ -8,12 +8,14 @@ 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
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
|
||||
/**
|
||||
@@ -21,12 +23,19 @@ import java.net.URLDecoder
|
||||
*/
|
||||
object CallLinks {
|
||||
private const val ROOT_KEY = "key"
|
||||
private const val EPOCH = "epoch"
|
||||
private const val HTTPS_LINK_PREFIX = "https://signal.link/call/#key="
|
||||
private const val SNGL_LINK_PREFIX = "sgnl://signal.link/call/#key="
|
||||
|
||||
private val TAG = Log.tag(CallLinks::class.java)
|
||||
|
||||
fun url(linkKeyBytes: ByteArray) = "$HTTPS_LINK_PREFIX${CallLinkRootKey(linkKeyBytes)}"
|
||||
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 watchCallLink(roomId: CallLinkRoomId): Observable<CallLinkTable.CallLink> {
|
||||
return Observable.create { emitter ->
|
||||
@@ -60,8 +69,13 @@ object CallLinks {
|
||||
return url.split("#").last().startsWith("key=")
|
||||
}
|
||||
|
||||
data class CallLinkParseResult(
|
||||
val rootKey: CallLinkRootKey,
|
||||
val epoch: CallLinkEpoch?
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun parseUrl(url: String): CallLinkRootKey? {
|
||||
fun parseUrl(url: String): CallLinkParseResult? {
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
Log.w(TAG, "Invalid url prefix.")
|
||||
return null
|
||||
@@ -73,18 +87,33 @@ object CallLinks {
|
||||
return null
|
||||
}
|
||||
|
||||
val fragment = parts[1]
|
||||
val fragmentParts = fragment.split("&")
|
||||
val fragmentQuery = fragmentParts.associate {
|
||||
val kv = it.split("=")
|
||||
if (kv.size != 2) {
|
||||
Log.w(TAG, "Invalid fragment keypair. Skipping.")
|
||||
val fragmentQuery = mutableMapOf<String, String?>()
|
||||
|
||||
try {
|
||||
for (part in parts[1].split("&")) {
|
||||
val kv = part.split("=")
|
||||
// Make sure we don't have an empty key (i.e. handle the case
|
||||
// of "a=0&&b=0", for example)
|
||||
if (kv[0].isEmpty()) {
|
||||
Log.w(TAG, "Invalid url: $url (empty key)")
|
||||
return null
|
||||
}
|
||||
val key = URLDecoder.decode(kv[0], "utf8")
|
||||
val value = when (kv.size) {
|
||||
1 -> null
|
||||
2 -> URLDecoder.decode(kv[1], "utf8")
|
||||
else -> {
|
||||
// Cannot have more than one value per key (i.e. handle the case
|
||||
// of "a=0&b=0=1=2", for example.
|
||||
Log.w(TAG, "Invalid url: $url (multiple values)")
|
||||
return null
|
||||
}
|
||||
}
|
||||
fragmentQuery += key to value
|
||||
}
|
||||
|
||||
val key = URLDecoder.decode(kv[0], "utf8")
|
||||
val value = URLDecoder.decode(kv[1], "utf8")
|
||||
|
||||
key to value
|
||||
} catch (_: UnsupportedEncodingException) {
|
||||
Log.w(TAG, "Invalid url: $url")
|
||||
return null
|
||||
}
|
||||
|
||||
val key = fragmentQuery[ROOT_KEY]
|
||||
@@ -94,9 +123,13 @@ object CallLinks {
|
||||
}
|
||||
|
||||
return try {
|
||||
CallLinkRootKey(key)
|
||||
val epoch = fragmentQuery[EPOCH]?.let { s -> CallLinkEpoch(s) }
|
||||
CallLinkParseResult(
|
||||
rootKey = CallLinkRootKey(key),
|
||||
epoch = epoch
|
||||
)
|
||||
} catch (e: CallException) {
|
||||
Log.w(TAG, "Invalid root key found in fragment query string.")
|
||||
Log.w(TAG, "Invalid root key or epoch found in fragment query string.")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(5, 6, 7, 8))
|
||||
val credentials = CallLinkCredentials(byteArrayOf(1, 2, 3, 4), byteArrayOf(0, 1, 2, 3), 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) } ?: ""
|
||||
callLink.credentials?.let { CallLinks.url(it.linkKeyBytes, it.epochBytes) } ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
startActivity(
|
||||
ShareActivity.sendSimpleText(
|
||||
requireContext(),
|
||||
getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.linkKeyBytes))
|
||||
getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes))
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -177,7 +177,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
lifecycleDisposable += viewModel.commitCallLink().subscribeBy(onSuccess = {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> {
|
||||
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes))
|
||||
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes))
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
|
||||
is EnsureCallLinkCreatedResult.Success -> {
|
||||
val mimeType = Intent.normalizeMimeType("text/plain")
|
||||
val shareIntent = ShareCompat.IntentBuilder(requireContext())
|
||||
.setText(CallLinks.url(viewModel.linkKeyBytes))
|
||||
.setText(CallLinks.url(viewModel.linkKeyBytes, viewModel.epochBytes))
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ class CreateCallLinkViewModel(
|
||||
private val repository: CreateCallLinkRepository = CreateCallLinkRepository(),
|
||||
private val mutationRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
|
||||
) : ViewModel() {
|
||||
private val credentials = CallLinkCredentials.generate()
|
||||
private val initialCredentials = CallLinkCredentials.generate()
|
||||
private val _callLink: MutableState<CallLinkTable.CallLink> = mutableStateOf(
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = credentials.roomId,
|
||||
credentials = credentials,
|
||||
roomId = initialCredentials.roomId,
|
||||
credentials = initialCredentials,
|
||||
state = SignalCallLinkState(
|
||||
name = "",
|
||||
restrictions = Restrictions.ADMIN_APPROVAL,
|
||||
@@ -48,7 +48,12 @@ class CreateCallLinkViewModel(
|
||||
)
|
||||
|
||||
val callLink: State<CallLinkTable.CallLink> = _callLink
|
||||
val linkKeyBytes: ByteArray = credentials.linkKeyBytes
|
||||
|
||||
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
|
||||
@@ -59,7 +64,7 @@ class CreateCallLinkViewModel(
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
disposables += CallLinks.watchCallLink(credentials.roomId)
|
||||
disposables += CallLinks.watchCallLink(initialCredentials.roomId)
|
||||
.subscribeBy {
|
||||
_callLink.value = it
|
||||
}
|
||||
@@ -75,7 +80,7 @@ class CreateCallLinkViewModel(
|
||||
}
|
||||
|
||||
fun commitCallLink(): Single<EnsureCallLinkCreatedResult> {
|
||||
return repository.ensureCallLinkCreated(credentials)
|
||||
return repository.ensureCallLinkCreated(initialCredentials)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
@@ -84,7 +89,7 @@ class CreateCallLinkViewModel(
|
||||
.flatMap {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> mutationRepository.setCallRestrictions(
|
||||
credentials,
|
||||
callLink.value.credentials!!,
|
||||
if (approveAllMembers) Restrictions.ADMIN_APPROVAL else Restrictions.NONE
|
||||
)
|
||||
is EnsureCallLinkCreatedResult.Failure -> Single.just(UpdateCallLinkResult.Failure(it.failure.status))
|
||||
@@ -104,7 +109,7 @@ class CreateCallLinkViewModel(
|
||||
.flatMap {
|
||||
when (it) {
|
||||
is EnsureCallLinkCreatedResult.Success -> mutationRepository.setCallName(
|
||||
credentials,
|
||||
callLink.value.credentials!!,
|
||||
callName
|
||||
)
|
||||
is EnsureCallLinkCreatedResult.Failure -> Single.just(UpdateCallLinkResult.Failure(it.failure.status))
|
||||
|
||||
@@ -118,7 +118,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
override fun onShareClicked() {
|
||||
val mimeType = Intent.normalizeMimeType("text/plain")
|
||||
val shareIntent = ShareCompat.IntentBuilder(requireContext())
|
||||
.setText(CallLinks.url(viewModel.rootKeySnapshot))
|
||||
.setText(CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot))
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
|
||||
@@ -130,7 +130,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
}
|
||||
|
||||
override fun onCopyClicked() {
|
||||
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.rootKeySnapshot))
|
||||
Util.copyToClipboard(requireContext(), CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot))
|
||||
Toast.makeText(requireContext(), R.string.CreateCallLinkBottomSheetDialogFragment__copied_to_clipboard, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ class CallLinkDetailsFragment : ComposeFragment(), CallLinkDetailsCallback {
|
||||
startActivity(
|
||||
ShareActivity.sendSimpleText(
|
||||
requireContext(),
|
||||
getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.rootKeySnapshot))
|
||||
getString(R.string.CreateCallLink__use_this_link_to_join_a_signal_call, CallLinks.url(viewModel.rootKeySnapshot, viewModel.epochSnapshot))
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -230,6 +230,7 @@ private fun CallLinkDetailsPreview() {
|
||||
val callLink = remember {
|
||||
val credentials = CallLinkCredentials(
|
||||
byteArrayOf(1, 2, 3, 4),
|
||||
byteArrayOf(0, 1, 2, 3),
|
||||
byteArrayOf(3, 4, 5, 6)
|
||||
)
|
||||
CallLinkTable.CallLink(
|
||||
|
||||
@@ -40,6 +40,9 @@ 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
|
||||
|
||||
@@ -172,11 +172,11 @@ public class LinkPreviewView extends FrameLayout {
|
||||
spinner.setVisibility(GONE);
|
||||
noPreview.setVisibility(GONE);
|
||||
|
||||
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
|
||||
CallLinks.CallLinkParseResult linkParseResult = CallLinks.parseUrl(linkPreview.getUrl());
|
||||
if (!Util.isEmpty(linkPreview.getTitle())) {
|
||||
title.setText(linkPreview.getTitle());
|
||||
title.setVisibility(VISIBLE);
|
||||
} else if (callLinkRootKey != null) {
|
||||
} else if (linkParseResult != 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 (callLinkRootKey != null) {
|
||||
} else if (linkParseResult != 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 (callLinkRootKey != null) {
|
||||
} else if (linkParseResult != null) {
|
||||
thumbnail.setVisibility(VISIBLE);
|
||||
thumbnailState.applyState(thumbnail);
|
||||
thumbnail.get().setImageDrawable(
|
||||
requestManager,
|
||||
new FallbackAvatarDrawable(
|
||||
getContext(),
|
||||
new FallbackAvatar.Resource.CallLink(AvatarColorHash.forCallLink(callLinkRootKey.getKeyBytes()))
|
||||
new FallbackAvatar.Resource.CallLink(AvatarColorHash.forCallLink(linkParseResult.getRootKey().getKeyBytes()))
|
||||
).circleCrop()
|
||||
);
|
||||
thumbnail.get().showSecondaryText(false);
|
||||
|
||||
@@ -34,6 +34,8 @@ 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) {
|
||||
|
||||
@@ -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))
|
||||
.setText(CallLinks.url(controlsAndInfoViewModel.rootKeySnapshot, controlsAndInfoViewModel.epochSnapshot))
|
||||
.setType(mimeType)
|
||||
.createChooserIntent()
|
||||
|
||||
|
||||
@@ -1172,14 +1172,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
//noinspection ConstantConditions
|
||||
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
||||
|
||||
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
|
||||
if (callLinkRootKey != null) {
|
||||
CallLinks.CallLinkParseResult linkParseResult = CallLinks.parseUrl(linkPreview.getUrl());
|
||||
if (linkParseResult != null) {
|
||||
joinCallLinkStub.setVisibility(View.VISIBLE);
|
||||
joinCallLinkStub.get().setTextColor(ContextCompat.getColor(context, messageRecord.isOutgoing() ? R.color.signal_light_colorOnPrimary : R.color.signal_colorOnPrimaryContainer));
|
||||
joinCallLinkStub.get().setBackgroundColor(ContextCompat.getColor(context, messageRecord.isOutgoing() ? R.color.signal_light_colorTransparent2 : R.color.signal_colorOnPrimary));
|
||||
joinCallLinkStub.get().setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onJoinCallLink(callLinkRootKey);
|
||||
eventListener.onJoinCallLink(linkParseResult.getRootKey(), linkParseResult.getEpoch());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ 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
|
||||
@@ -3365,8 +3366,8 @@ class ConversationFragment :
|
||||
GroupDescriptionDialog.show(childFragmentManager, groupName, description, shouldLinkifyWebLinks)
|
||||
}
|
||||
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
|
||||
CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey) {
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) {
|
||||
CommunicationActions.startVideoCall(this@ConversationFragment, callLinkRootKey, callLinkEpoch) {
|
||||
YouAreAlreadyInACallSnackbar.show(requireView())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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
|
||||
@@ -47,6 +48,7 @@ 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"
|
||||
@@ -61,6 +63,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY,
|
||||
$ROOT_KEY BLOB,
|
||||
$EPOCH BLOB,
|
||||
$ROOM_ID TEXT NOT NULL UNIQUE,
|
||||
$ADMIN_KEY BLOB,
|
||||
$NAME TEXT NOT NULL,
|
||||
@@ -128,6 +131,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
.values(
|
||||
contentValuesOf(
|
||||
ROOT_KEY to credentials.linkKeyBytes,
|
||||
EPOCH to credentials.epochBytes,
|
||||
ADMIN_KEY to credentials.adminPassBytes
|
||||
)
|
||||
)
|
||||
@@ -188,7 +192,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
}
|
||||
|
||||
fun getOrCreateCallLinkByRootKey(
|
||||
callLinkRootKey: CallLinkRootKey
|
||||
callLinkRootKey: CallLinkRootKey,
|
||||
callLinkEpoch: CallLinkEpoch?
|
||||
): CallLink {
|
||||
val roomId = CallLinkRoomId.fromBytes(callLinkRootKey.deriveRoomId())
|
||||
val callLink = getCallLinkByRoomId(roomId)
|
||||
@@ -198,6 +203,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
roomId = roomId,
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = callLinkRootKey.keyBytes,
|
||||
epochBytes = callLinkEpoch?.bytes,
|
||||
adminPassBytes = null
|
||||
),
|
||||
state = SignalCallLinkState(),
|
||||
@@ -207,12 +213,26 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
insertCallLink(link)
|
||||
return getCallLinkByRoomId(roomId)!!
|
||||
} else {
|
||||
callLink
|
||||
if (callLink.credentials?.epoch != callLinkEpoch) {
|
||||
overwriteEpoch(callLink, callLinkEpoch)
|
||||
} else {
|
||||
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
|
||||
@@ -227,6 +247,7 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
roomId = roomId,
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = callLinkRootKey.keyBytes,
|
||||
epochBytes = callLinkEpoch?.bytes,
|
||||
adminPassBytes = adminPassKey
|
||||
),
|
||||
state = SignalCallLinkState(),
|
||||
@@ -253,7 +274,8 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
writableDatabase.update(TABLE_NAME)
|
||||
.values(
|
||||
ADMIN_KEY to adminPassKey,
|
||||
ROOT_KEY to callLinkRootKey.keyBytes
|
||||
ROOT_KEY to callLinkRootKey.keyBytes,
|
||||
EPOCH to callLinkEpoch?.bytes
|
||||
)
|
||||
.where("$ROOM_ID = ?", callLink.roomId.serialize())
|
||||
.run()
|
||||
@@ -464,6 +486,7 @@ 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())
|
||||
@@ -487,6 +510,7 @@ 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)
|
||||
)
|
||||
},
|
||||
|
||||
@@ -139,6 +139,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V281_RemoveArchiveT
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V282_AddSnippetMessageIdColumnToThreadTable
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V283_ViewOnceRemoteDataCleanup
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V284_SetPlaceholderGroupFlag
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V285_AddEpochToCallLinksTable
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
|
||||
|
||||
/**
|
||||
@@ -283,10 +284,11 @@ object SignalDatabaseMigrations {
|
||||
281 to V281_RemoveArchiveTransferFile,
|
||||
282 to V282_AddSnippetMessageIdColumnToThreadTable,
|
||||
283 to V283_ViewOnceRemoteDataCleanup,
|
||||
284 to V284_SetPlaceholderGroupFlag
|
||||
284 to V284_SetPlaceholderGroupFlag,
|
||||
285 to V285_AddEpochToCallLinksTable
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 284
|
||||
const val DATABASE_VERSION = 285
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2025 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
|
||||
|
||||
@Suppress("ClassName")
|
||||
object V285_AddEpochToCallLinksTable : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL("ALTER TABLE call_link ADD COLUMN epoch BLOB DEFAULT NULL")
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ 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()
|
||||
)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
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.protos.groups.local.DecryptedGroupJoinInfo;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -326,15 +327,20 @@ public class LinkPreviewRepository {
|
||||
@NonNull String callLinkUrl,
|
||||
@NonNull Callback callback) {
|
||||
|
||||
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(callLinkUrl);
|
||||
if (callLinkRootKey == null) {
|
||||
CallLinks.CallLinkParseResult linkParseResult = CallLinks.parseUrl(callLinkUrl);
|
||||
if (linkParseResult == 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(callLinkRootKey.getKeyBytes(), null))
|
||||
.readCallLink(new CallLinkCredentials(linkParseResult.getRootKey().getKeyBytes(),
|
||||
epochBytes,
|
||||
null))
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
result -> {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -358,7 +359,7 @@ class MessageDetailsFragment : FullScreenDialogFragment(), MessageDetailsAdapter
|
||||
Log.w(TAG, "Not yet implemented!", Exception())
|
||||
}
|
||||
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey, callLinkEpoch: CallLinkEpoch?) {
|
||||
Log.w(TAG, "Not yet implemented!", Exception())
|
||||
}
|
||||
|
||||
|
||||
@@ -1370,6 +1370,7 @@ object SyncMessageProcessor {
|
||||
roomId,
|
||||
CallLinkCredentials(
|
||||
callLinkUpdate.rootKey!!.toByteArray(),
|
||||
callLinkUpdate.epoch?.toByteArray(),
|
||||
callLinkUpdate.adminPasskey?.toByteArray()
|
||||
)
|
||||
)
|
||||
@@ -1381,6 +1382,7 @@ object SyncMessageProcessor {
|
||||
roomId = roomId,
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = callLinkRootKey.keyBytes,
|
||||
epochBytes = callLinkUpdate.epoch?.toByteArray(),
|
||||
adminPassBytes = callLinkUpdate.adminPasskey?.toByteArray()
|
||||
),
|
||||
state = SignalCallLinkState(),
|
||||
|
||||
@@ -70,7 +70,7 @@ class CallLinkPreJoinActionProcessor(
|
||||
serverPublicParams.endorsementPublicKey,
|
||||
callLinkAuthCredentialPresentation.serialize(),
|
||||
callLinkRootKey,
|
||||
null,
|
||||
callLink.credentials.epoch,
|
||||
callLink.credentials.adminPassBytes,
|
||||
ByteArray(0),
|
||||
AUDIO_LEVELS_INTERVAL,
|
||||
|
||||
@@ -25,6 +25,7 @@ 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.GroupCall;
|
||||
@@ -406,6 +407,7 @@ 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());
|
||||
@@ -417,7 +419,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
|
||||
CallLinkSecretParams.deriveFromRootKey(callLinkRootKey.getKeyBytes())
|
||||
);
|
||||
|
||||
callManager.peekCallLinkCall(SignalStore.internal().getGroupCallingServer(), callLinkAuthCredentialPresentation.serialize(), callLinkRootKey, null, peekInfo -> {
|
||||
callManager.peekCallLinkCall(SignalStore.internal().getGroupCallingServer(), callLinkAuthCredentialPresentation.serialize(), callLinkRootKey, callLinkEpoch, peekInfo -> {
|
||||
PeekInfo info = peekInfo.getValue();
|
||||
if (info == null) {
|
||||
Log.w(TAG, "Failed to get peek info: " + peekInfo.getStatus());
|
||||
|
||||
@@ -7,6 +7,7 @@ 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
|
||||
|
||||
/**
|
||||
@@ -15,6 +16,7 @@ import org.signal.ringrtc.CallLinkRootKey
|
||||
@Parcelize
|
||||
data class CallLinkCredentials(
|
||||
val linkKeyBytes: ByteArray,
|
||||
val epochBytes: ByteArray?,
|
||||
val adminPassBytes: ByteArray?
|
||||
) : Parcelable {
|
||||
|
||||
@@ -22,6 +24,10 @@ 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
|
||||
@@ -35,6 +41,12 @@ 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
|
||||
}
|
||||
@@ -52,6 +64,7 @@ data class CallLinkCredentials(
|
||||
fun generate(): CallLinkCredentials {
|
||||
return CallLinkCredentials(
|
||||
CallLinkRootKey.generate().keyBytes,
|
||||
null,
|
||||
CallLinkRootKey.generateAdminPasskey()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,9 +120,10 @@ class SignalCallLinkManager(
|
||||
) { result ->
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "Successfully created call link.")
|
||||
val epoch = result.value!!.epoch
|
||||
emitter.onSuccess(
|
||||
CreateCallLinkResult.Success(
|
||||
credentials = CallLinkCredentials(rootKey.keyBytes, adminPassKey),
|
||||
credentials = CallLinkCredentials(rootKey.keyBytes, epoch?.bytes, adminPassKey),
|
||||
state = result.value!!.toAppState()
|
||||
)
|
||||
)
|
||||
@@ -142,7 +143,7 @@ class SignalCallLinkManager(
|
||||
SignalStore.internal.groupCallingServer,
|
||||
requestCallLinkAuthCredentialPresentation(credentials.linkKeyBytes).serialize(),
|
||||
CallLinkRootKey(credentials.linkKeyBytes),
|
||||
null
|
||||
credentials.epoch
|
||||
) {
|
||||
if (it.isSuccess) {
|
||||
emitter.onSuccess(ReadCallLinkResult.Success(it.value!!.toAppState()))
|
||||
@@ -169,7 +170,7 @@ class SignalCallLinkManager(
|
||||
SignalStore.internal.groupCallingServer,
|
||||
credentialPresentation.serialize(),
|
||||
CallLinkRootKey(credentials.linkKeyBytes),
|
||||
null,
|
||||
credentials.epoch,
|
||||
credentials.adminPassBytes,
|
||||
name
|
||||
) { result ->
|
||||
@@ -197,7 +198,7 @@ class SignalCallLinkManager(
|
||||
SignalStore.internal.groupCallingServer,
|
||||
credentialPresentation.serialize(),
|
||||
CallLinkRootKey(credentials.linkKeyBytes),
|
||||
null,
|
||||
credentials.epoch,
|
||||
credentials.adminPassBytes,
|
||||
restrictions
|
||||
) { result ->
|
||||
@@ -224,7 +225,7 @@ class SignalCallLinkManager(
|
||||
SignalStore.internal.groupCallingServer,
|
||||
credentialPresentation.serialize(),
|
||||
CallLinkRootKey(credentials.linkKeyBytes),
|
||||
null,
|
||||
credentials.epoch,
|
||||
credentials.adminPassBytes
|
||||
) { result ->
|
||||
if (result.isSuccess && result.value == true) {
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
package org.thoughtcrime.securesms.storage
|
||||
|
||||
import okio.ByteString
|
||||
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
|
||||
@@ -46,8 +48,10 @@ class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCallLinkReco
|
||||
val callLink = SignalDatabase.callLinks.getCallLinkByRoomId(roomId)
|
||||
|
||||
if (callLink != null && callLink.credentials?.adminPassBytes != null) {
|
||||
val epochBytes = callLink.credentials.epochBytes
|
||||
return SignalCallLinkRecord.newBuilder(null).apply {
|
||||
rootKey = callRootKey.keyBytes.toByteString()
|
||||
epoch = epochBytes?.toByteString() ?: ByteString.EMPTY
|
||||
adminPasskey = callLink.credentials.adminPassBytes.toByteString()
|
||||
deletedAtTimestampMs = callLink.deletionTimestamp
|
||||
}.build().toSignalCallLinkRecord(StorageId.forCallLink(keyGenerator.generate())).toOptional()
|
||||
@@ -88,8 +92,15 @@ class CallLinkRecordProcessor : DefaultStorageRecordProcessor<SignalCallLinkReco
|
||||
private fun insertOrUpdateRecord(record: SignalCallLinkRecord) {
|
||||
val rootKey = CallLinkRootKey(record.proto.rootKey.toByteArray())
|
||||
|
||||
val epoch = if (record.proto.epoch.isNotEmpty()) {
|
||||
CallLinkEpoch.fromBytes(record.proto.epoch.toByteArray())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
SignalDatabase.callLinks.insertOrUpdateCallLinkByRootKey(
|
||||
callLinkRootKey = rootKey,
|
||||
callLinkEpoch = epoch,
|
||||
adminPassKey = record.proto.adminPasskey.toByteArray(),
|
||||
deletionTimestamp = record.proto.deletedAtTimestampMs,
|
||||
storageId = record.id
|
||||
|
||||
@@ -27,6 +27,7 @@ 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;
|
||||
@@ -325,8 +326,8 @@ public class CommunicationActions {
|
||||
return;
|
||||
}
|
||||
|
||||
CallLinkRootKey rootKey = CallLinks.parseUrl(potentialUrl);
|
||||
if (rootKey == null) {
|
||||
CallLinks.CallLinkParseResult linkParseResult = CallLinks.parseUrl(potentialUrl);
|
||||
if (linkParseResult == null) {
|
||||
Log.w(TAG, "Failed to parse root key from call link");
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setTitle(R.string.CommunicationActions_invalid_link)
|
||||
@@ -336,7 +337,7 @@ public class CommunicationActions {
|
||||
return;
|
||||
}
|
||||
|
||||
startVideoCall(new ActivityCallContext(activity), rootKey, onUserAlreadyInAnotherCall);
|
||||
startVideoCall(new ActivityCallContext(activity), linkParseResult.getRootKey(), linkParseResult.getEpoch(), onUserAlreadyInAnotherCall);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,14 +346,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, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
|
||||
startVideoCall(new FragmentCallContext(fragment), rootKey, onUserAlreadyInAnotherCall);
|
||||
public static void startVideoCall(@NonNull Fragment fragment, @NonNull CallLinkRootKey rootKey, @Nullable CallLinkEpoch epoch, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
|
||||
startVideoCall(new FragmentCallContext(fragment), rootKey, epoch, onUserAlreadyInAnotherCall);
|
||||
}
|
||||
|
||||
private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
|
||||
private static void startVideoCall(@NonNull CallContext callContext, @NonNull CallLinkRootKey rootKey, @Nullable CallLinkEpoch epoch, @NonNull OnUserAlreadyInAnotherCall onUserAlreadyInAnotherCall) {
|
||||
SimpleTask.run(() -> {
|
||||
CallLinkRoomId roomId = CallLinkRoomId.fromBytes(rootKey.deriveRoomId());
|
||||
CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey);
|
||||
CallLinkTable.CallLink callLink = SignalDatabase.callLinks().getOrCreateCallLinkByRootKey(rootKey, epoch);
|
||||
|
||||
if (callLink.getState().hasBeenRevoked()) {
|
||||
return Optional.<Recipient>empty();
|
||||
|
||||
@@ -344,6 +344,7 @@ message CallLink {
|
||||
string name = 3;
|
||||
Restrictions restrictions = 4;
|
||||
uint64 expirationMs = 5;
|
||||
bytes epoch = 6; // May be absent/empty for older links
|
||||
}
|
||||
|
||||
message AdHocCall {
|
||||
|
||||
@@ -34,6 +34,7 @@ class CallLinkRecordProcessorTest {
|
||||
private val testSubject = CallLinkRecordProcessor()
|
||||
private val mockCredentials = CallLinkCredentials(
|
||||
"root key".toByteArray(),
|
||||
"abcd".toByteArray(),
|
||||
"admin pass".toByteArray()
|
||||
)
|
||||
|
||||
|
||||
@@ -628,6 +628,7 @@ message SyncMessage {
|
||||
optional bytes rootKey = 1;
|
||||
optional bytes adminPasskey = 2;
|
||||
optional Type type = 3; // defaults to UPDATE
|
||||
optional bytes epoch = 4;
|
||||
}
|
||||
|
||||
message CallLogEvent {
|
||||
|
||||
@@ -306,6 +306,7 @@ message CallLinkRecord {
|
||||
bytes rootKey = 1;
|
||||
bytes adminPasskey = 2;
|
||||
uint64 deletedAtTimestampMs = 3;
|
||||
bytes epoch = 4;
|
||||
}
|
||||
|
||||
message Recipient {
|
||||
|
||||
Reference in New Issue
Block a user