Add support for group call disposition.

Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
Alex Hart
2023-04-03 10:44:14 -03:00
parent e94a84d4ec
commit f9548dcffe
40 changed files with 2165 additions and 340 deletions

View File

@@ -2,24 +2,34 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.database.Cursor
import androidx.annotation.Discouraged
import androidx.core.content.contentValuesOf
import org.signal.core.util.IntSerializer
import org.signal.core.util.Serializer
import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.readToSingleLong
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireLong
import org.signal.core.util.requireObject
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.ringrtc.CallId
import org.signal.ringrtc.CallManager.RingUpdate
import org.thoughtcrime.securesms.calls.log.CallLogFilter
import org.thoughtcrime.securesms.calls.log.CallLogRow
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.CallSyncEventJob
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent
import java.util.UUID
/**
* Contains details for each 1:1 call.
@@ -37,16 +47,23 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
private const val TYPE = "type"
private const val DIRECTION = "direction"
private const val EVENT = "event"
private const val TIMESTAMP = "timestamp"
private const val RINGER = "ringer"
private const val DELETION_TIMESTAMP = "deletion_timestamp"
//language=sql
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$CALL_ID INTEGER NOT NULL UNIQUE,
$MESSAGE_ID INTEGER NOT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE CASCADE,
$PEER INTEGER NOT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
$MESSAGE_ID INTEGER DEFAULT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE SET NULL,
$PEER INTEGER DEFAULT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
$TYPE INTEGER NOT NULL,
$DIRECTION INTEGER NOT NULL,
$EVENT INTEGER NOT NULL
$EVENT INTEGER NOT NULL,
$TIMESTAMP INTEGER NOT NULL,
$RINGER INTEGER DEFAULT NULL,
$DELETION_TIMESTAMP INTEGER DEFAULT 0
)
""".trimIndent()
@@ -56,7 +73,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
)
}
fun insertCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event) {
fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event) {
val messageType: Long = Call.getMessageType(type, direction, event)
writableDatabase.withinTransaction {
@@ -68,7 +85,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
PEER to peer.serialize(),
TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(direction),
EVENT to Event.serialize(event)
EVENT to Event.serialize(event),
TIMESTAMP to timestamp
)
writableDatabase.insert(TABLE_NAME, null, values)
@@ -79,7 +97,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
Log.i(TAG, "Inserted call: $callId type: $type direction: $direction event:$event")
}
fun updateCall(callId: Long, event: Event): Call? {
fun updateOneToOneCall(callId: Long, event: Event): Call? {
return writableDatabase.withinTransaction {
writableDatabase
.update(TABLE_NAME)
@@ -97,7 +115,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
if (call != null) {
Log.i(TAG, "Updated call: $callId event: $event")
SignalDatabase.messages.updateCallLog(call.messageId, call.messageType)
SignalDatabase.messages.updateCallLog(call.messageId!!, call.messageType)
ApplicationDependencies.getMessageNotifier().updateNotification(context)
}
@@ -131,7 +149,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
val cursor = readableDatabase
.select()
.from(TABLE_NAME)
.where(query.where, query.whereArgs)
.where("$EVENT != ${Event.serialize(Event.DELETE)} AND ${query.where}", query.whereArgs)
.run()
calls.putAll(cursor.readToList { c -> c.requireLong(MESSAGE_ID) to Call.deserialize(c) })
@@ -139,9 +157,536 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
return calls
}
fun getOldestDeletionTimestamp(): Long {
return writableDatabase
.select(DELETION_TIMESTAMP)
.from(TABLE_NAME)
.where("$DELETION_TIMESTAMP > 0")
.orderBy("$DELETION_TIMESTAMP DESC")
.limit(1)
.run()
.readToSingleLong(0L)
}
fun deleteCallEventsDeletedBefore(threshold: Long) {
writableDatabase
.delete(TABLE_NAME)
.where("$DELETION_TIMESTAMP <= ?", threshold)
.run()
}
/**
* If a non-ad-hoc call has been deleted from the message database, then we need to
* set its deletion_timestamp to now.
*/
fun updateCallEventDeletionTimestamps() {
val where = "$TYPE != ? AND $DELETION_TIMESTAMP = 0 AND $MESSAGE_ID IS NULL"
val type = Type.serialize(Type.AD_HOC_CALL)
val toSync = writableDatabase.withinTransaction { db ->
val result = db
.select()
.from(TABLE_NAME)
.where(where, type)
.run()
.readToList {
Call.deserialize(it)
}
.toSet()
db
.update(TABLE_NAME)
.values(DELETION_TIMESTAMP to System.currentTimeMillis())
.where(where, type)
.run()
result
}
CallSyncEventJob.enqueueDeleteSyncEvents(toSync)
ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
}
// region Group / Ad-Hoc Calling
fun deleteGroupCall(call: Call) {
checkIsGroupOrAdHocCall(call)
writableDatabase.withinTransaction { db ->
db
.update(TABLE_NAME)
.values(
EVENT to Event.serialize(Event.DELETE),
DELETION_TIMESTAMP to System.currentTimeMillis()
)
.where("$CALL_ID = ?", call.callId)
.run()
if (call.messageId != null) {
SignalDatabase.messages.deleteMessage(call.messageId)
}
}
ApplicationDependencies.getMessageNotifier().updateNotification(context)
Log.d(TAG, "Marked group call event for deletion: ${call.callId}")
}
fun insertDeletedGroupCallFromSyncEvent(
callId: Long,
recipientId: RecipientId?,
direction: Direction,
timestamp: Long
) {
val type = if (recipientId != null) Type.GROUP_CALL else Type.AD_HOC_CALL
writableDatabase
.insertInto(TABLE_NAME)
.values(
CALL_ID to callId,
MESSAGE_ID to null,
PEER to recipientId?.toLong(),
EVENT to Event.serialize(Event.DELETE),
TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
DELETION_TIMESTAMP to System.currentTimeMillis()
)
.run()
ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary()
}
fun acceptIncomingGroupCall(call: Call) {
checkIsGroupOrAdHocCall(call)
check(call.direction == Direction.INCOMING)
val newEvent = when (call.event) {
Event.RINGING, Event.MISSED, Event.DECLINED -> Event.ACCEPTED
Event.GENERIC_GROUP_CALL -> Event.JOINED
else -> {
Log.d(TAG, "Call in state ${call.event} cannot be transitioned by ACCEPTED")
return
}
}
writableDatabase
.update(TABLE_NAME)
.values(EVENT to Event.serialize(newEvent))
.run()
ApplicationDependencies.getMessageNotifier().updateNotification(context)
Log.d(TAG, "Transitioned group call ${call.callId} from ${call.event} to $newEvent")
}
fun insertAcceptedGroupCall(
callId: Long,
recipientId: RecipientId?,
direction: Direction,
timestamp: Long
) {
val type = if (recipientId != null) Type.GROUP_CALL else Type.AD_HOC_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
writableDatabase.withinTransaction { db ->
val messageId: MessageId? = if (recipientId != null) {
SignalDatabase.messages.insertGroupCall(
groupRecipientId = recipientId,
sender = Recipient.self().id,
timestamp,
"",
emptyList(),
false
)
} else {
null
}
db
.insertInto(TABLE_NAME)
.values(
CALL_ID to callId,
MESSAGE_ID to messageId?.id,
PEER to recipientId?.toLong(),
EVENT to Event.serialize(event),
TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
RINGER to ringer
)
.run()
}
}
fun insertOrUpdateGroupCallFromExternalEvent(
groupRecipientId: RecipientId,
sender: RecipientId,
timestamp: Long,
messageGroupCallEraId: String?
) {
insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId,
sender,
timestamp,
messageGroupCallEraId,
emptyList(),
false
)
}
fun insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId: RecipientId,
sender: RecipientId,
timestamp: Long,
peekGroupCallEraId: String?,
peekJoinedUuids: Collection<UUID>,
isCallFull: Boolean
) {
writableDatabase.withinTransaction {
if (peekGroupCallEraId.isNullOrEmpty()) {
Log.w(TAG, "Dropping local call event with null era id.")
return@withinTransaction
}
val callId = CallId.fromEra(peekGroupCallEraId).longValue()
val call = getCallById(callId)
val messageId: MessageId = if (call != null) {
if (call.event == Event.DELETE) {
Log.d(TAG, "Dropping group call update for deleted call.")
return@withinTransaction
}
if (call.type != Type.GROUP_CALL) {
Log.d(TAG, "Dropping unsupported update message for non-group-call call.")
return@withinTransaction
}
if (call.messageId == null) {
Log.d(TAG, "Dropping group call update for call without an attached message.")
return@withinTransaction
}
SignalDatabase.messages.updateGroupCall(
call.messageId,
peekGroupCallEraId,
peekJoinedUuids,
isCallFull
)
} else {
SignalDatabase.messages.insertGroupCall(
groupRecipientId,
sender,
timestamp,
peekGroupCallEraId,
peekJoinedUuids,
isCallFull
)
}
insertCallEventFromGroupUpdate(
callId,
messageId,
sender,
groupRecipientId,
timestamp
)
}
}
private fun insertCallEventFromGroupUpdate(
callId: Long,
messageId: MessageId?,
sender: RecipientId,
groupRecipientId: RecipientId,
timestamp: Long
) {
if (messageId != null) {
val call = getCallById(callId)
if (call == null) {
val direction = if (sender == Recipient.self().id) Direction.OUTGOING else Direction.INCOMING
writableDatabase
.insertInto(TABLE_NAME)
.values(
CALL_ID to callId,
MESSAGE_ID to messageId.id,
PEER to groupRecipientId.toLong(),
EVENT to Event.serialize(Event.GENERIC_GROUP_CALL),
TYPE to Type.serialize(Type.GROUP_CALL),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
RINGER to null
)
.run()
Log.d(TAG, "Inserted new call event from group call update message. Call Id: $callId")
} else {
if (timestamp < call.timestamp) {
setTimestamp(callId, timestamp)
Log.d(TAG, "Updated call event timestamp for call id $callId")
}
if (call.messageId == null) {
setMessageId(callId, messageId)
Log.d(TAG, "Updated call event message id for newly inserted group call state: $callId")
}
}
} else {
Log.d(TAG, "Skipping call event processing for null era id.")
}
}
/**
* Since this does not alter the call table, we can simply pass this directly through to the old handler.
*/
fun updateGroupCallFromPeek(
threadId: Long,
peekGroupCallEraId: String?,
peekJoinedUuids: Collection<UUID>,
isCallFull: Boolean
): Boolean {
return SignalDatabase.messages.updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull)
}
fun insertOrUpdateGroupCallFromRingState(
ringId: Long,
groupRecipientId: RecipientId,
ringerRecipient: RecipientId,
dateReceived: Long,
ringState: RingUpdate
) {
handleGroupRingState(ringId, groupRecipientId, ringerRecipient, dateReceived, ringState)
}
fun insertOrUpdateGroupCallFromRingState(
ringId: Long,
groupRecipientId: RecipientId,
ringerUUID: UUID,
dateReceived: Long,
ringState: RingUpdate
) {
val ringerRecipient = Recipient.externalPush(ServiceId.from(ringerUUID))
handleGroupRingState(ringId, groupRecipientId, ringerRecipient.id, dateReceived, ringState)
}
fun isRingCancelled(ringId: Long): Boolean {
val call = getCallById(ringId) ?: return false
return call.event != Event.RINGING
}
private fun handleGroupRingState(
ringId: Long,
groupRecipientId: RecipientId,
ringerRecipient: RecipientId,
dateReceived: Long,
ringState: RingUpdate
) {
val call = getCallById(ringId)
if (call != null) {
if (call.event == Event.DELETE) {
Log.d(TAG, "Ignoring ring request for $ringId since its event has been deleted.")
return
}
when (ringState) {
RingUpdate.REQUESTED -> {
when (call.event) {
Event.GENERIC_GROUP_CALL -> updateEventFromRingState(ringId, Event.RINGING, ringerRecipient)
Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED, ringerRecipient)
else -> Log.w(TAG, "Received a REQUESTED ring event while in ${call.event}. Ignoring.")
}
}
RingUpdate.EXPIRED_REQUEST, RingUpdate.CANCELLED_BY_RINGER -> {
when (call.event) {
Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED, ringerRecipient)
Event.OUTGOING_RING -> Log.w(TAG, "Received an expiration or cancellation while in OUTGOING_RING state. Ignoring.")
else -> Unit
}
}
RingUpdate.BUSY_LOCALLY, RingUpdate.BUSY_ON_ANOTHER_DEVICE -> {
when (call.event) {
Event.JOINED -> updateEventFromRingState(ringId, Event.ACCEPTED)
Event.GENERIC_GROUP_CALL, Event.RINGING -> updateEventFromRingState(ringId, Event.MISSED)
else -> Log.w(TAG, "Received a busy event we can't process. Ignoring.")
}
}
RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE -> {
updateEventFromRingState(ringId, Event.ACCEPTED)
}
RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> {
when (call.event) {
Event.RINGING, Event.MISSED -> updateEventFromRingState(ringId, Event.DECLINED)
Event.OUTGOING_RING -> Log.w(TAG, "Received DECLINED_ON_ANOTHER_DEVICE while in OUTGOING_RING state.")
else -> Unit
}
}
}
} else {
val event: Event = when (ringState) {
RingUpdate.REQUESTED -> Event.RINGING
RingUpdate.EXPIRED_REQUEST -> Event.MISSED
RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE -> {
Log.w(TAG, "Missed original ring request for $ringId")
Event.ACCEPTED
}
RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> {
Log.w(TAG, "Missed original ring request for $ringId")
Event.DECLINED
}
RingUpdate.BUSY_LOCALLY, RingUpdate.BUSY_ON_ANOTHER_DEVICE -> {
Log.w(TAG, "Missed original ring request for $ringId")
Event.MISSED
}
RingUpdate.CANCELLED_BY_RINGER -> {
Log.w(TAG, "Missed original ring request for $ringId")
Event.MISSED
}
}
createEventFromRingState(ringId, groupRecipientId, ringerRecipient, event, dateReceived)
}
}
private fun updateEventFromRingState(
callId: Long,
event: Event,
ringerRecipient: RecipientId
) {
writableDatabase
.update(TABLE_NAME)
.values(
EVENT to Event.serialize(event),
RINGER to ringerRecipient.serialize()
)
.where("$CALL_ID = ?", callId)
.run()
Log.d(TAG, "Updated ring state to $event")
}
private fun updateEventFromRingState(
callId: Long,
event: Event
) {
writableDatabase
.update(TABLE_NAME)
.values(
EVENT to Event.serialize(event)
)
.where("$CALL_ID = ?", callId)
.run()
Log.d(TAG, "Updated ring state to $event")
}
private fun createEventFromRingState(
callId: Long,
groupRecipientId: RecipientId,
ringerRecipient: RecipientId,
event: Event,
timestamp: Long
) {
val direction = if (ringerRecipient == Recipient.self().id) Direction.OUTGOING else Direction.INCOMING
writableDatabase.withinTransaction { db ->
val messageId = SignalDatabase.messages.insertGroupCall(
groupRecipientId = groupRecipientId,
sender = ringerRecipient,
timestamp = timestamp,
eraId = "",
joinedUuids = emptyList(),
isCallFull = false
)
db
.insertInto(TABLE_NAME)
.values(
CALL_ID to callId,
MESSAGE_ID to messageId.id,
PEER to groupRecipientId.toLong(),
EVENT to Event.serialize(event),
TYPE to Type.serialize(Type.GROUP_CALL),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
RINGER to ringerRecipient.toLong()
)
.run()
}
Log.d(TAG, "Inserted a new call event for $callId with event $event")
}
fun setTimestamp(callId: Long, timestamp: Long) {
writableDatabase.withinTransaction { db ->
val call = getCallById(callId)
if (call == null || call.event == Event.DELETE) {
Log.d(TAG, "Refusing to update deleted call event.")
return@withinTransaction
}
db
.update(TABLE_NAME)
.values(TIMESTAMP to timestamp)
.where("$CALL_ID = ?", callId)
.run()
if (call.messageId != null) {
SignalDatabase.messages.updateCallTimestamps(call.messageId, timestamp)
}
}
}
private fun setMessageId(callId: Long, messageId: MessageId) {
writableDatabase
.update(TABLE_NAME)
.values(MESSAGE_ID to messageId.id)
.where("$CALL_ID = ?", callId)
.run()
}
fun deleteCallEvents(callIds: Set<Long>) {
val messageIds = getMessageIds(callIds)
SignalDatabase.messages.deleteCallUpdates(messageIds)
updateCallEventDeletionTimestamps()
}
fun deleteAllCallEventsExcept(callIds: Set<Long>) {
val messageIds = getMessageIds(callIds)
SignalDatabase.messages.deleteAllCallUpdatesExcept(messageIds)
updateCallEventDeletionTimestamps()
}
@Discouraged("Using this method is generally considered an error. Utilize other deletion methods instead of this.")
fun deleteAllCalls() {
Log.w(TAG, "Deleting all calls from the local database.")
writableDatabase
.delete(TABLE_NAME)
.run()
}
private fun getMessageIds(callIds: Set<Long>): Set<Long> {
val queries = SqlUtil.buildCollectionQuery(
CALL_ID,
callIds,
"$MESSAGE_ID NOT NULL AND"
)
return queries.map { query ->
readableDatabase.select(MESSAGE_ID).from(TABLE_NAME).where(query.where, query.whereArgs).run().readToList {
it.requireLong(MESSAGE_ID)
}
}.flatten().toSet()
}
private fun checkIsGroupOrAdHocCall(call: Call) {
check(call.type == Type.GROUP_CALL || call.type == Type.AD_HOC_CALL)
}
// endregion
private fun getCallsCursor(isCount: Boolean, offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): Cursor {
val filterClause = when (filter) {
CallLogFilter.ALL -> SqlUtil.buildQuery("")
CallLogFilter.ALL -> SqlUtil.buildQuery("$EVENT != ${Event.serialize(Event.DELETE)}")
CallLogFilter.MISSED -> SqlUtil.buildQuery("$EVENT == ${Event.serialize(Event.MISSED)}")
}
@@ -233,12 +778,22 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
val type: Type,
val direction: Direction,
val event: Event,
val messageId: Long
val messageId: Long?,
val timestamp: Long,
val ringerRecipient: RecipientId?
) {
val messageType: Long = getMessageType(type, direction, event)
companion object Deserializer : Serializer<Call, Cursor> {
fun getMessageType(type: Type, direction: Direction, event: Event): Long {
if (type == Type.GROUP_CALL) {
return MessageTypes.GROUP_CALL_TYPE
}
if (type == Type.AD_HOC_CALL) {
error("Ad-Hoc calls are not linked to messages.")
}
return if (direction == Direction.INCOMING && event == Event.MISSED) {
if (type == Type.VIDEO_CALL) MessageTypes.MISSED_VIDEO_CALL_TYPE else MessageTypes.MISSED_AUDIO_CALL_TYPE
} else if (direction == Direction.INCOMING) {
@@ -259,7 +814,15 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
type = data.requireObject(TYPE, Type.Serializer),
direction = data.requireObject(DIRECTION, Direction.Serializer),
event = data.requireObject(EVENT, Event.Serializer),
messageId = data.requireLong(MESSAGE_ID)
messageId = data.requireLong(MESSAGE_ID).takeIf { it > 0L },
timestamp = data.requireLong(TIMESTAMP),
ringerRecipient = data.requireLong(RINGER).let {
if (it > 0) {
RecipientId.from(it)
} else {
null
}
}
)
}
}
@@ -267,7 +830,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
enum class Type(private val code: Int) {
AUDIO_CALL(0),
VIDEO_CALL(1);
VIDEO_CALL(1),
GROUP_CALL(3),
AD_HOC_CALL(4);
companion object Serializer : IntSerializer<Type> {
override fun serialize(data: Type): Int = data.code
@@ -276,6 +841,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
return when (data) {
AUDIO_CALL.code -> AUDIO_CALL
VIDEO_CALL.code -> VIDEO_CALL
GROUP_CALL.code -> GROUP_CALL
AD_HOC_CALL.code -> AD_HOC_CALL
else -> throw IllegalArgumentException("Unknown type $data")
}
}
@@ -286,6 +853,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
CallEvent.Type.UNKNOWN_TYPE -> null
CallEvent.Type.AUDIO_CALL -> AUDIO_CALL
CallEvent.Type.VIDEO_CALL -> VIDEO_CALL
CallEvent.Type.GROUP_CALL -> GROUP_CALL
CallEvent.Type.AD_HOC_CALL -> AD_HOC_CALL
}
}
}
@@ -318,22 +887,69 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
}
enum class Event(private val code: Int) {
/**
* 1:1 Calls only.
*/
ONGOING(0),
/**
* 1:1 and Group Calls.
*
* Group calls: You accepted a ring.
*/
ACCEPTED(1),
/**
* 1:1 Calls only.
*/
NOT_ACCEPTED(2),
MISSED(3);
/**
* 1:1 and Group/Ad-Hoc Calls.
*
* Group calls: The remote ring has expired or was cancelled by the ringer.
*/
MISSED(3),
/**
* 1:1 and Group/Ad-Hoc Calls.
*/
DELETE(4),
/**
* Group/Ad-Hoc Calls only.
*
* Initial state.
*/
GENERIC_GROUP_CALL(5),
/**
* Group Calls: User has joined the group call.
*/
JOINED(6),
/**
* Group Calls: If a ring was requested by another user.
*/
RINGING(7),
/**
* Group Calls: If you declined a ring.
*/
DECLINED(8),
/**
* Group Calls: If you are ringing a group.
*/
OUTGOING_RING(9);
companion object Serializer : IntSerializer<Event> {
override fun serialize(data: Event): Int = data.code
override fun deserialize(data: Int): Event {
return when (data) {
ONGOING.code -> ONGOING
ACCEPTED.code -> ACCEPTED
NOT_ACCEPTED.code -> NOT_ACCEPTED
MISSED.code -> MISSED
else -> throw IllegalArgumentException("Unknown type $data")
}
return values().firstOrNull {
it.code == data
} ?: throw IllegalArgumentException("Unknown event $data")
}
@JvmStatic
@@ -342,6 +958,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
CallEvent.Event.UNKNOWN_ACTION -> null
CallEvent.Event.ACCEPTED -> ACCEPTED
CallEvent.Event.NOT_ACCEPTED -> NOT_ACCEPTED
CallEvent.Event.DELETE -> DELETE
}
}
}

View File

@@ -1,98 +0,0 @@
package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil
import org.signal.ringrtc.CallManager
import java.util.concurrent.TimeUnit
/**
* Track state of Group Call ring cancellations.
*/
class GroupCallRingTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
companion object {
private val VALID_RING_DURATION = TimeUnit.MINUTES.toMillis(30)
private const val TABLE_NAME = "group_call_ring"
private const val ID = "_id"
private const val RING_ID = "ring_id"
private const val DATE_RECEIVED = "date_received"
private const val RING_STATE = "ring_state"
@JvmField
val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$RING_ID INTEGER UNIQUE,
$DATE_RECEIVED INTEGER,
$RING_STATE INTEGER
)
""".trimIndent()
@JvmField
val CREATE_INDEXES = arrayOf(
"CREATE INDEX date_received_index on $TABLE_NAME ($DATE_RECEIVED)"
)
}
fun isCancelled(ringId: Long): Boolean {
val db = databaseHelper.signalReadableDatabase
db.query(TABLE_NAME, null, "$RING_ID = ?", SqlUtil.buildArgs(ringId), null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
return CursorUtil.requireInt(cursor, RING_STATE) != 0
}
}
return false
}
fun insertGroupRing(ringId: Long, dateReceived: Long, ringState: CallManager.RingUpdate) {
val db = databaseHelper.signalWritableDatabase
val values = ContentValues().apply {
put(RING_ID, ringId)
put(DATE_RECEIVED, dateReceived)
put(RING_STATE, ringState.toCode())
}
db.insert(TABLE_NAME, null, values)
removeOldRings()
}
fun insertOrUpdateGroupRing(ringId: Long, dateReceived: Long, ringState: CallManager.RingUpdate) {
val db = databaseHelper.signalWritableDatabase
val values = ContentValues().apply {
put(RING_ID, ringId)
put(DATE_RECEIVED, dateReceived)
put(RING_STATE, ringState.toCode())
}
db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE)
removeOldRings()
}
fun removeOldRings() {
val db = databaseHelper.signalWritableDatabase
db.delete(TABLE_NAME, "$DATE_RECEIVED < ?", SqlUtil.buildArgs(System.currentTimeMillis() - VALID_RING_DURATION))
}
fun deleteAll() {
databaseHelper.signalWritableDatabase.delete(TABLE_NAME, null, null)
}
}
private fun CallManager.RingUpdate.toCode(): Int {
return when (this) {
CallManager.RingUpdate.REQUESTED -> 0
CallManager.RingUpdate.EXPIRED_REQUEST -> 1
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE -> 2
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE -> 3
CallManager.RingUpdate.BUSY_LOCALLY -> 4
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE -> 5
CallManager.RingUpdate.CANCELLED_BY_RINGER -> 6
}
}

View File

@@ -38,7 +38,6 @@ import org.signal.core.util.SqlUtil.buildSingleCollectionQuery
import org.signal.core.util.SqlUtil.buildTrueUpdateQuery
import org.signal.core.util.SqlUtil.getNextAutoIncrementId
import org.signal.core.util.delete
import org.signal.core.util.emptyIfNull
import org.signal.core.util.exists
import org.signal.core.util.forEach
import org.signal.core.util.insertInto
@@ -71,6 +70,7 @@ import org.thoughtcrime.securesms.conversation.MessageStyler
import org.thoughtcrime.securesms.database.EarlyReceiptCache.Receipt
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.distributionLists
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groupReceipts
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
@@ -400,6 +400,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
($TYPE = ${MessageTypes.MISSED_AUDIO_CALL_TYPE})
OR
($TYPE = ${MessageTypes.MISSED_VIDEO_CALL_TYPE})
OR
($TYPE = ${MessageTypes.GROUP_CALL_TYPE})
)""".toSingleLine()
@JvmStatic
@@ -802,122 +804,111 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(MessageId(messageId))
}
fun insertOrUpdateGroupCall(
fun insertGroupCall(
groupRecipientId: RecipientId,
sender: RecipientId,
timestamp: Long,
peekGroupCallEraId: String?,
peekJoinedUuids: Collection<UUID>,
eraId: String,
joinedUuids: Collection<UUID>,
isCallFull: Boolean
) {
): MessageId {
val recipient = Recipient.resolved(groupRecipientId)
val threadId = threads.getOrCreateThreadIdFor(recipient)
val peerEraIdSameAsPrevious = updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull)
val messageId: MessageId = writableDatabase.withinTransaction { db ->
val self = Recipient.self()
val markRead = joinedUuids.contains(self.requireServiceId().uuid()) || self.id == sender
val updateDetails: ByteArray = GroupCallUpdateDetails.newBuilder()
.setEraId(eraId)
.setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString())
.setStartedCallTimestamp(timestamp)
.addAllInCallUuids(joinedUuids.map { it.toString() })
.setIsCallFull(isCallFull)
.build()
.toByteArray()
writableDatabase.withinTransaction { db ->
if (!peerEraIdSameAsPrevious && !Util.isEmpty(peekGroupCallEraId)) {
val self = Recipient.self()
val markRead = peekJoinedUuids.contains(self.requireServiceId().uuid()) || self.id == sender
val updateDetails = GroupCallUpdateDetails.newBuilder()
.setEraId(peekGroupCallEraId.emptyIfNull())
.setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString())
.setStartedCallTimestamp(timestamp)
.addAllInCallUuids(peekJoinedUuids.map { it.toString() }.toList())
.setIsCallFull(isCallFull)
.build()
.toByteArray()
val values = contentValuesOf(
RECIPIENT_ID to sender.serialize(),
RECIPIENT_DEVICE_ID to 1,
DATE_RECEIVED to timestamp,
DATE_SENT to timestamp,
READ to if (markRead) 1 else 0,
BODY to Base64.encodeBytes(updateDetails),
TYPE to MessageTypes.GROUP_CALL_TYPE,
THREAD_ID to threadId
)
db.insert(TABLE_NAME, null, values)
threads.incrementUnread(threadId, 1, 0)
}
val values = contentValuesOf(
RECIPIENT_ID to sender.serialize(),
RECIPIENT_DEVICE_ID to 1,
DATE_RECEIVED to timestamp,
DATE_SENT to timestamp,
READ to if (markRead) 1 else 0,
BODY to Base64.encodeBytes(updateDetails),
TYPE to MessageTypes.GROUP_CALL_TYPE,
THREAD_ID to threadId
)
val messageId = MessageId(db.insert(TABLE_NAME, null, values))
threads.incrementUnread(threadId, 1, 0)
threads.update(threadId, true)
messageId
}
notifyConversationListeners(threadId)
TrimThreadJob.enqueueAsync(threadId)
return messageId
}
fun insertOrUpdateGroupCall(
groupRecipientId: RecipientId,
sender: RecipientId,
timestamp: Long,
messageGroupCallEraId: String?
) {
val threadId = writableDatabase.withinTransaction { db ->
val recipient = Recipient.resolved(groupRecipientId)
val threadId = threads.getOrCreateThreadIdFor(recipient)
val cursor = db
.select(*MMS_PROJECTION)
.from(TABLE_NAME)
.where("$TYPE = ? AND $THREAD_ID = ?", MessageTypes.GROUP_CALL_TYPE, threadId)
.orderBy("$DATE_RECEIVED DESC")
.limit(1)
.run()
var sameEraId = false
MmsReader(cursor).use { reader ->
val record: MessageRecord? = reader.firstOrNull()
if (record != null) {
val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body)
sameEraId = groupCallUpdateDetails.eraId == messageGroupCallEraId && !Util.isEmpty(messageGroupCallEraId)
if (!sameEraId) {
db.update(TABLE_NAME)
.values(BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, emptyList(), false))
.where("$ID = ?", record.id)
.run()
}
}
}
if (!sameEraId && !Util.isEmpty(messageGroupCallEraId)) {
val updateDetails = GroupCallUpdateDetails.newBuilder()
.setEraId(Util.emptyIfNull(messageGroupCallEraId))
.setStartedCallUuid(Recipient.resolved(sender).requireServiceId().toString())
.setStartedCallTimestamp(timestamp)
.addAllInCallUuids(emptyList())
.setIsCallFull(false)
.build()
.toByteArray()
val values = contentValuesOf(
RECIPIENT_ID to sender.serialize(),
RECIPIENT_DEVICE_ID to 1,
DATE_RECEIVED to timestamp,
DATE_SENT to timestamp,
READ to 0,
BODY to Base64.encodeBytes(updateDetails),
TYPE to MessageTypes.GROUP_CALL_TYPE,
THREAD_ID to threadId
)
db.insert(TABLE_NAME, null, values)
threads.incrementUnread(threadId, 1, 0)
}
threads.update(threadId, true)
threadId
/**
* Updates the timestamps associated with the given message id to the given ts
*/
fun updateCallTimestamps(messageId: Long, timestamp: Long) {
val message = try {
getMessageRecord(messageId = messageId)
} catch (e: NoSuchMessageException) {
error("Message $messageId does not exist")
}
notifyConversationListeners(threadId)
TrimThreadJob.enqueueAsync(threadId)
val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body)
val contentValues = contentValuesOf(
BODY to Base64.encodeBytes(updateDetail.toBuilder().setStartedCallTimestamp(timestamp).build().toByteArray()),
DATE_SENT to timestamp,
DATE_RECEIVED to timestamp
)
val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(messageId), contentValues)
val updated = writableDatabase.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0
if (updated) {
notifyConversationListeners(message.threadId)
}
}
fun updateGroupCall(
messageId: Long,
eraId: String,
joinedUuids: Collection<UUID>,
isCallFull: Boolean
): MessageId {
writableDatabase.withinTransaction { db ->
val message = try {
getMessageRecord(messageId = messageId)
} catch (e: NoSuchMessageException) {
error("Message $messageId does not exist.")
}
val updateDetail = GroupCallUpdateDetailsUtil.parse(message.body)
val containsSelf = joinedUuids.contains(SignalStore.account().requireAci().uuid())
val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId)
val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList()
val contentValues = contentValuesOf(
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull)
)
if (sameEraId && containsSelf) {
contentValues.put(READ, 1)
}
val query = buildTrueUpdateQuery(ID_WHERE, buildArgs(messageId), contentValues)
val updated = db.update(TABLE_NAME, contentValues, query.where, query.whereArgs) > 0
if (updated) {
notifyConversationListeners(message.threadId)
}
}
return MessageId(messageId)
}
fun updatePreviousGroupCall(threadId: Long, peekGroupCallEraId: String?, peekJoinedUuids: Collection<UUID>, isCallFull: Boolean): Boolean {
@@ -3099,6 +3090,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
.where("$ID = ?", messageId)
.run()
calls.updateCallEventDeletionTimestamps()
threads.setLastScrolled(threadId, 0)
val threadDeleted = threads.update(threadId, false)
@@ -3356,6 +3348,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
if (deletes > 0) {
Log.i(TAG, "Deleted $deletes abandoned messages")
calls.updateCallEventDeletionTimestamps()
}
return deletes
@@ -3385,6 +3378,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
groupReceipts.deleteAllRows()
mentions.deleteAllMentions()
writableDatabase.delete(TABLE_NAME).run()
calls.updateCallEventDeletionTimestamps()
OptimizeMessageSearchIndexJob.enqueue()
}

View File

@@ -65,7 +65,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val emojiSearchTable: EmojiSearchTable = EmojiSearchTable(context, this)
val messageSendLogTables: MessageSendLogTables = MessageSendLogTables(context, this)
val avatarPickerDatabase: AvatarPickerDatabase = AvatarPickerDatabase(context, this)
val groupCallRingTable: GroupCallRingTable = GroupCallRingTable(context, this)
val reactionTable: ReactionTable = ReactionTable(context, this)
val notificationProfileDatabase: NotificationProfileDatabase = NotificationProfileDatabase(context, this)
val donationReceiptTable: DonationReceiptTable = DonationReceiptTable(context, this)
@@ -103,7 +102,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
db.execSQL(ChatColorsTable.CREATE_TABLE)
db.execSQL(EmojiSearchTable.CREATE_TABLE)
db.execSQL(AvatarPickerDatabase.CREATE_TABLE)
db.execSQL(GroupCallRingTable.CREATE_TABLE)
db.execSQL(ReactionTable.CREATE_TABLE)
db.execSQL(DonationReceiptTable.CREATE_TABLE)
db.execSQL(StorySendTable.CREATE_TABLE)
@@ -129,7 +127,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
executeStatements(db, MentionTable.CREATE_INDEXES)
executeStatements(db, PaymentTable.CREATE_INDEXES)
executeStatements(db, MessageSendLogTables.CREATE_INDEXES)
executeStatements(db, GroupCallRingTable.CREATE_INDEXES)
executeStatements(db, NotificationProfileDatabase.CREATE_INDEXES)
executeStatements(db, DonationReceiptTable.CREATE_INDEXS)
executeStatements(db, StorySendTable.CREATE_INDEXS)
@@ -389,11 +386,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
val emojiSearch: EmojiSearchTable
get() = instance!!.emojiSearchTable
@get:JvmStatic
@get:JvmName("groupCallRings")
val groupCallRings: GroupCallRingTable
get() = instance!!.groupCallRingTable
@get:JvmStatic
@get:JvmName("groupReceipts")
val groupReceipts: GroupReceiptTable

View File

@@ -31,8 +31,8 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.database.MessageTable.MarkedMessageInfo
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.drafts
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groupCallRings
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groupReceipts
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.mentions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
@@ -380,6 +380,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
setLastScrolled(threadId, 0)
update(threadId, false)
notifyConversationListeners(threadId)
SignalDatabase.calls.updateCallEventDeletionTimestamps()
} else {
Log.i(TAG, "Trimming deleted no messages thread: $threadId")
}
@@ -1081,13 +1082,14 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
ConversationUtil.clearShortcuts(context, recipientIds)
}
@SuppressLint("DiscouragedApi")
fun deleteAllConversations() {
writableDatabase.withinTransaction { db ->
messageLog.deleteAll()
messages.deleteAllThreads()
drafts.clearAllDrafts()
groupCallRings.deleteAll()
db.delete(TABLE_NAME, null, null)
calls.deleteAllCalls()
}
notifyConversationListListeners()

View File

@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V178_ReportingToken
import org.thoughtcrime.securesms.database.helpers.migration.V179_CleanupDanglingMessageSendLogMigration
import org.thoughtcrime.securesms.database.helpers.migration.V180_RecipientNicknameMigration
import org.thoughtcrime.securesms.database.helpers.migration.V181_ThreadTableForeignKeyCleanup
import org.thoughtcrime.securesms.database.helpers.migration.V182_CallTableMigration
/**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@@ -45,7 +46,7 @@ object SignalDatabaseMigrations {
val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
const val DATABASE_VERSION = 181
const val DATABASE_VERSION = 182
@JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -180,6 +181,10 @@ object SignalDatabaseMigrations {
if (oldVersion < 181) {
V181_ThreadTableForeignKeyCleanup.migrate(context, db, oldVersion, newVersion)
}
if (oldVersion < 182) {
V182_CallTableMigration.migrate(context, db, oldVersion, newVersion)
}
}
@JvmStatic

View File

@@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.RecipientTable
/**
* Adds a new 'timestamp' column to CallTable and copies in the date_sent column data from
* the messages database.
*
* Adds a new 'ringer' column to the CallTable setting each entry to NULL. This is safe since up
* to this point we were not using the table for group calls. This is effectively a replacement for
* the GroupCallRing table.
*
* Removes the 'NOT NULL' condition on message_id and peer, as with ad-hoc calling in place, these
* can now be null.
*/
object V182_CallTableMigration : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL(
"""
CREATE TABLE call_tmp (
_id INTEGER PRIMARY KEY,
call_id INTEGER NOT NULL UNIQUE,
message_id INTEGER DEFAULT NULL REFERENCES ${MessageTable.TABLE_NAME} (${MessageTable.ID}) ON DELETE SET NULL,
peer INTEGER DEFAULT NULL REFERENCES ${RecipientTable.TABLE_NAME} (${RecipientTable.ID}) ON DELETE CASCADE,
type INTEGER NOT NULL,
direction INTEGER NOT NULL,
event INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
ringer INTEGER DEFAULT NULL,
deletion_timestamp INTEGER DEFAULT 0
)
""".trimIndent()
)
db.execSQL(
"""
INSERT INTO call_tmp
SELECT
_id,
call_id,
message_id,
peer,
type,
direction,
event,
(SELECT date_sent FROM message WHERE message._id = call.message_id) as timestamp,
NULL as ringer,
0 as deletion_timestamp
FROM call
""".trimIndent()
)
db.execSQL("DROP TABLE group_call_ring")
db.execSQL("DROP TABLE call")
db.execSQL("ALTER TABLE call_tmp RENAME TO call")
}
}