mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Add support for group call disposition.
Co-authored-by: Cody Henthorne <cody@signal.org>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user