mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Integrate call links create/update/read apis.
This commit is contained in:
committed by
Nicholas Tinsley
parent
4d6d31d624
commit
5a38143987
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user