Integrate call links create/update/read apis.

This commit is contained in:
Alex Hart
2023-05-19 10:28:29 -03:00
committed by Nicholas Tinsley
parent 4d6d31d624
commit 5a38143987
60 changed files with 1986 additions and 191 deletions

View File

@@ -1,11 +1,34 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import androidx.core.content.contentValuesOf
import org.signal.core.util.Serializer
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.readToSingleInt
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullBlob
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.ringrtc.CallLinkState.Restrictions
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
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.thoughtcrime.securesms.util.Base64
import java.time.Instant
import java.time.temporal.ChronoUnit
/**
* Table containing ad-hoc call link details
@@ -42,13 +65,168 @@ class CallLinkTable(context: Context, databaseHelper: SignalDatabase) : Database
$RECIPIENT_ID INTEGER UNIQUE REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE
)
"""
private fun SignalCallLinkState.serialize(): ContentValues {
return contentValuesOf(
NAME to name,
RESTRICTIONS to restrictions.mapToInt(),
EXPIRATION to expiration.toEpochMilli(),
REVOKED to revoked
)
}
private fun Restrictions.mapToInt(): Int {
return when (this) {
Restrictions.NONE -> 0
Restrictions.ADMIN_APPROVAL -> 1
Restrictions.UNKNOWN -> 2
}
}
}
fun insertCallLink(
callLink: CallLink
) {
writableDatabase.withinTransaction { db ->
val recipientId = SignalDatabase.recipients.getOrInsertFromCallLinkRoomId(callLink.roomId)
db
.insertInto(TABLE_NAME)
.values(CallLinkSerializer.serialize(callLink.copy(recipientId = recipientId)))
.run()
}
ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(callLink.roomId)
}
fun updateCallLinkCredentials(
roomId: CallLinkRoomId,
credentials: CallLinkCredentials
) {
writableDatabase
.update(TABLE_NAME)
.values(
contentValuesOf(
ROOT_KEY to credentials.linkKeyBytes,
ADMIN_KEY to credentials.adminPassBytes
)
)
.where("$ROOM_ID = ?", roomId.serialize())
.run()
ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(roomId)
}
fun updateCallLinkState(
roomId: CallLinkRoomId,
state: SignalCallLinkState
) {
writableDatabase
.update(TABLE_NAME)
.values(state.serialize())
.where("$ROOM_ID = ?", roomId.serialize())
.run()
ApplicationDependencies.getDatabaseObserver().notifyCallLinkObservers(roomId)
}
fun callLinkExists(
callLinkRoomId: CallLinkRoomId
): Boolean {
return writableDatabase
.select("COUNT(*)")
.from(TABLE_NAME)
.where("$ROOM_ID = ?", callLinkRoomId.serialize())
.run()
.readToSingleInt() > 0
}
fun getCallLinkByRoomId(
callLinkRoomId: CallLinkRoomId
): CallLink? {
return writableDatabase
.select()
.from(TABLE_NAME)
.where("$ROOM_ID = ?", callLinkRoomId.serialize())
.run()
.readToSingleObject { CallLinkDeserializer.deserialize(it) }
}
fun getOrCreateCallLinkByRoomId(
callLinkRoomId: CallLinkRoomId
): CallLink {
val callLink = getCallLinkByRoomId(callLinkRoomId)
return if (callLink == null) {
val link = CallLink(
recipientId = RecipientId.UNKNOWN,
roomId = callLinkRoomId,
credentials = null,
state = SignalCallLinkState(),
avatarColor = AvatarColor.random()
)
insertCallLink(link)
return getCallLinkByRoomId(callLinkRoomId)!!
} else {
callLink
}
}
private object CallLinkSerializer : Serializer<CallLink, ContentValues> {
override fun serialize(data: CallLink): ContentValues {
return contentValuesOf(
RECIPIENT_ID to data.recipientId.takeIf { it != RecipientId.UNKNOWN }?.toLong(),
ROOM_ID to data.roomId.serialize(),
ROOT_KEY to data.credentials?.linkKeyBytes,
ADMIN_KEY to data.credentials?.adminPassBytes,
AVATAR_COLOR to data.avatarColor.serialize()
).apply {
putAll(data.state.serialize())
}
}
override fun deserialize(data: ContentValues): CallLink {
throw UnsupportedOperationException()
}
}
private object CallLinkDeserializer : Serializer<CallLink, Cursor> {
override fun serialize(data: CallLink): Cursor {
throw UnsupportedOperationException()
}
override fun deserialize(data: Cursor): CallLink {
return CallLink(
recipientId = data.requireLong(RECIPIENT_ID).let { if (it > 0) RecipientId.from(it) else RecipientId.UNKNOWN },
roomId = CallLinkRoomId.fromBytes(Base64.decode(data.requireNonNullString(ROOM_ID))),
credentials = CallLinkCredentials(
linkKeyBytes = data.requireNonNullBlob(ROOT_KEY),
adminPassBytes = data.requireBlob(ADMIN_KEY)
),
state = SignalCallLinkState(
name = data.requireNonNullString(NAME),
restrictions = data.requireInt(RESTRICTIONS).mapToRestrictions(),
revoked = data.requireBoolean(REVOKED),
expiration = Instant.ofEpochMilli(data.requireLong(EXPIRATION)).truncatedTo(ChronoUnit.DAYS)
),
avatarColor = AvatarColor.deserialize(data.requireString(AVATAR_COLOR))
)
}
private fun Int.mapToRestrictions(): Restrictions {
return when (this) {
0 -> Restrictions.NONE
1 -> Restrictions.ADMIN_APPROVAL
else -> Restrictions.UNKNOWN
}
}
}
data class CallLink(
val name: String,
val identifier: String,
val avatarColor: AvatarColor,
val isApprovalRequired: Boolean
val recipientId: RecipientId,
val roomId: CallLinkRoomId,
val credentials: CallLinkCredentials?,
val state: SignalCallLinkState,
val avatarColor: AvatarColor
)
override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {

View File

@@ -244,10 +244,11 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
}
// region Group / Ad-Hoc Calling
fun deleteGroupCall(call: Call) {
checkIsGroupOrAdHocCall(call)
val filter: SqlUtil.Query = getCallSelectionQuery(call.callId, call.peer)
writableDatabase.withinTransaction { db ->
db
.update(TABLE_NAME)
@@ -255,7 +256,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
EVENT to Event.serialize(Event.DELETE),
DELETION_TIMESTAMP to System.currentTimeMillis()
)
.where("$CALL_ID = ? AND $PEER = ?", call.callId, call.peer)
.where(filter.where, filter.whereArgs)
.run()
if (call.messageId != null) {
@@ -274,7 +275,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
direction: Direction,
timestamp: Long
) {
val type = Type.GROUP_CALL
val recipient = Recipient.resolved(recipientId)
val type = if (recipient.isCallLink) Type.AD_HOC_CALL else Type.GROUP_CALL
writableDatabase
.insertInto(TABLE_NAME)
@@ -322,7 +324,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
direction: Direction,
timestamp: Long
) {
val type = Type.GROUP_CALL
val recipient = Recipient.resolved(recipientId)
val type = if (recipient.isCallLink) Type.AD_HOC_CALL else Type.GROUP_CALL
val event = if (direction == Direction.OUTGOING) Event.OUTGOING_RING else Event.JOINED
val ringer = if (direction == Direction.OUTGOING) Recipient.self().id.toLong() else null
@@ -951,7 +954,6 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
fun getCalls(offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): List<CallLogRow.Call> {
return getCallsCursor(false, offset, limit, searchTerm, filter).readToList { cursor ->
val call = Call.deserialize(cursor)
val recipient = Recipient.resolved(call.peer)
val groupCallDetails = GroupCallUpdateDetailsUtil.parse(cursor.requireString(MessageTable.BODY))
val children = cursor.requireNonNullString("children")
@@ -969,8 +971,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
CallLogRow.Call(
record = call,
peer = recipient,
date = call.timestamp,
peer = Recipient.resolved(call.peer),
groupCallState = CallLogRow.GroupCallState.fromDetails(groupCallDetails),
children = actualChildren.toSet()
)

View File

@@ -10,6 +10,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId;
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
import java.util.Collection;
@@ -46,27 +47,28 @@ public class DatabaseObserver {
private static final String KEY_CONVERSATION_DELETES = "ConversationDeletes";
private static final String KEY_CALL_UPDATES = "CallUpdates";
private static final String KEY_CALL_LINK_UPDATES = "CallLinkUpdates";
private final Application application;
private final Executor executor;
private final Set<Observer> conversationListObservers;
private final Map<Long, Set<Observer>> conversationObservers;
private final Map<Long, Set<Observer>> verboseConversationObservers;
private final Map<Long, Set<Observer>> conversationDeleteObservers;
private final Map<UUID, Set<Observer>> paymentObservers;
private final Map<Long, Set<Observer>> scheduledMessageObservers;
private final Set<Observer> allPaymentsObservers;
private final Set<Observer> chatColorsObservers;
private final Set<Observer> stickerObservers;
private final Set<Observer> stickerPackObservers;
private final Set<Observer> attachmentObservers;
private final Set<MessageObserver> messageUpdateObservers;
private final Map<Long, Set<MessageObserver>> messageInsertObservers;
private final Set<Observer> notificationProfileObservers;
private final Map<RecipientId, Set<Observer>> storyObservers;
private final Set<Observer> callUpdateObservers;
private final Set<Observer> conversationListObservers;
private final Map<Long, Set<Observer>> conversationObservers;
private final Map<Long, Set<Observer>> verboseConversationObservers;
private final Map<Long, Set<Observer>> conversationDeleteObservers;
private final Map<UUID, Set<Observer>> paymentObservers;
private final Map<Long, Set<Observer>> scheduledMessageObservers;
private final Set<Observer> allPaymentsObservers;
private final Set<Observer> chatColorsObservers;
private final Set<Observer> stickerObservers;
private final Set<Observer> stickerPackObservers;
private final Set<Observer> attachmentObservers;
private final Set<MessageObserver> messageUpdateObservers;
private final Map<Long, Set<MessageObserver>> messageInsertObservers;
private final Set<Observer> notificationProfileObservers;
private final Map<RecipientId, Set<Observer>> storyObservers;
private final Set<Observer> callUpdateObservers;
private final Map<CallLinkRoomId, Set<Observer>> callLinkObservers;
public DatabaseObserver(Application application) {
this.application = application;
@@ -87,6 +89,7 @@ public class DatabaseObserver {
this.storyObservers = new HashMap<>();
this.scheduledMessageObservers = new HashMap<>();
this.callUpdateObservers = new HashSet<>();
this.callLinkObservers = new HashMap<>();
}
public void registerConversationListObserver(@NonNull Observer listener) {
@@ -186,6 +189,12 @@ public class DatabaseObserver {
executor.execute(() -> callUpdateObservers.add(observer));
}
public void registerCallLinkObserver(@NonNull CallLinkRoomId callLinkRoomId, @NonNull Observer observer) {
executor.execute(() -> {
registerMapped(callLinkObservers, callLinkRoomId, observer);
});
}
public void unregisterObserver(@NonNull Observer listener) {
executor.execute(() -> {
conversationListObservers.remove(listener);
@@ -201,6 +210,7 @@ public class DatabaseObserver {
unregisterMapped(scheduledMessageObservers, listener);
unregisterMapped(conversationDeleteObservers, listener);
callUpdateObservers.remove(listener);
unregisterMapped(callLinkObservers, listener);
});
}
@@ -342,6 +352,10 @@ public class DatabaseObserver {
runPostSuccessfulTransaction(KEY_CALL_UPDATES, () -> notifySet(callUpdateObservers));
}
public void notifyCallLinkObservers(@NonNull CallLinkRoomId callLinkRoomId) {
runPostSuccessfulTransaction(KEY_CALL_LINK_UPDATES, () -> notifyMapped(callLinkObservers, callLinkRoomId));
}
private void runPostSuccessfulTransaction(@NonNull String dedupeKey, @NonNull Runnable runnable) {
SignalDatabase.runPostSuccessfulTransaction(dedupeKey, () -> {
executor.execute(runnable);

View File

@@ -87,6 +87,7 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.storage.StorageSyncModels
@@ -421,6 +422,10 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
return getByColumn(SERVICE_ID, serviceId.toString())
}
fun getByCallLinkRoomId(callLinkRoomId: CallLinkRoomId): Optional<RecipientId> {
return getByColumn(CALL_LINK_ROOM_ID, callLinkRoomId.serialize())
}
/**
* Will return a recipient matching the PNI, but only in the explicit [PNI_COLUMN]. This should only be checked in conjunction with [getByServiceId] as a way
* to avoid creating a recipient we already merged.
@@ -559,6 +564,18 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
).recipientId
}
fun getOrInsertFromCallLinkRoomId(callLinkRoomId: CallLinkRoomId): RecipientId {
return getOrInsertByColumn(
CALL_LINK_ROOM_ID,
callLinkRoomId.serialize(),
contentValuesOf(
GROUP_TYPE to GroupType.CALL_LINK.id,
CALL_LINK_ROOM_ID to callLinkRoomId.serialize(),
PROFILE_SHARING to 1
)
).recipientId
}
fun getDistributionListRecipientIds(): List<RecipientId> {
val recipientIds = mutableListOf<RecipientId>()
readableDatabase.query(TABLE_NAME, arrayOf(ID), "$DISTRIBUTION_LIST_ID is not NULL", null, null, null, null).use { cursor ->

View File

@@ -74,6 +74,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val pendingPniSignatureMessageTable: PendingPniSignatureMessageTable = PendingPniSignatureMessageTable(context, this)
val callTable: CallTable = CallTable(context, this)
val kyberPreKeyTable: KyberPreKeyTable = KyberPreKeyTable(context, this)
val callLinkTable: CallLinkTable = CallLinkTable(context, this)
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)
@@ -536,5 +537,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
@get:JvmName("calls")
val calls: CallTable
get() = instance!!.callTable
@get:JvmStatic
@get:JvmName("callLinks")
val callLinks: CallLinkTable
get() = instance!!.callLinkTable
}
}