mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 10:46:50 +00:00
Add call disposition syncing.
This commit is contained in:
committed by
Greyson Parrelli
parent
d471647e12
commit
06b414f4ef
@@ -13,6 +13,7 @@ import org.signal.paging.PagedDataSource;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.CallTable;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
@@ -32,6 +33,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
@@ -98,6 +100,7 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
AttachmentHelper attachmentHelper = new AttachmentHelper();
|
||||
ReactionHelper reactionHelper = new ReactionHelper();
|
||||
PaymentHelper paymentHelper = new PaymentHelper();
|
||||
CallHelper callHelper = new CallHelper();
|
||||
Set<ServiceId> referencedIds = new HashSet<>();
|
||||
|
||||
try (MmsSmsTable.Reader reader = MmsSmsTable.readerFor(db.getConversation(threadId, start, length))) {
|
||||
@@ -109,6 +112,7 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
reactionHelper.add(record);
|
||||
attachmentHelper.add(record);
|
||||
paymentHelper.add(record);
|
||||
callHelper.add(record);
|
||||
|
||||
UpdateDescription description = record.getUpdateDisplayBody(context, null);
|
||||
if (description != null) {
|
||||
@@ -151,6 +155,12 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
records = paymentHelper.buildUpdatedModels(records);
|
||||
stopwatch.split("payment-models");
|
||||
|
||||
callHelper.fetchCalls();
|
||||
stopwatch.split("calls");
|
||||
|
||||
records = callHelper.buildUpdatedModels(records);
|
||||
stopwatch.split("call-models");
|
||||
|
||||
for (ServiceId serviceId : referencedIds) {
|
||||
Recipient.resolved(RecipientId.from(serviceId));
|
||||
}
|
||||
@@ -202,6 +212,15 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
}
|
||||
stopwatch.split("payments");
|
||||
|
||||
if (record.isCallLog() && !record.isGroupCall()) {
|
||||
CallTable.Call call = SignalDatabase.calls().getCallByMessageId(record.getId());
|
||||
if (call != null && record instanceof MediaMmsMessageRecord) {
|
||||
record = ((MediaMmsMessageRecord) record).withCall(call);
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.split("calls");
|
||||
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), record, mentions, isQuoted);
|
||||
} else {
|
||||
return null;
|
||||
@@ -365,4 +384,35 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
private static class CallHelper {
|
||||
private final Collection<Long> messageIds = new LinkedList<>();
|
||||
private Map<Long, CallTable.Call> messageIdToCall = Collections.emptyMap();
|
||||
|
||||
public void add(MessageRecord messageRecord) {
|
||||
if (messageRecord.isCallLog() && !messageRecord.isGroupCall()) {
|
||||
messageIds.add(messageRecord.getId());
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchCalls() {
|
||||
if (!messageIds.isEmpty()) {
|
||||
messageIdToCall = SignalDatabase.calls().getCalls(messageIds);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull List<MessageRecord> buildUpdatedModels(@NonNull List<MessageRecord> records) {
|
||||
return records.stream()
|
||||
.map(record -> {
|
||||
if (record.isCallLog() && record instanceof MediaMmsMessageRecord) {
|
||||
CallTable.Call call = messageIdToCall.get(record.getId());
|
||||
if (call != null) {
|
||||
return ((MediaMmsMessageRecord) record).withCall(call);
|
||||
}
|
||||
}
|
||||
return record;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
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.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
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.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent
|
||||
|
||||
/**
|
||||
* Contains details for each 1:1 call.
|
||||
*/
|
||||
class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper), RecipientIdDatabaseReference {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(CallTable::class.java)
|
||||
|
||||
private const val TABLE_NAME = "call"
|
||||
private const val ID = "_id"
|
||||
private const val CALL_ID = "call_id"
|
||||
private const val MESSAGE_ID = "message_id"
|
||||
private const val PEER = "peer"
|
||||
private const val TYPE = "type"
|
||||
private const val DIRECTION = "direction"
|
||||
private const val EVENT = "event"
|
||||
|
||||
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,
|
||||
$TYPE INTEGER NOT NULL,
|
||||
$DIRECTION INTEGER NOT NULL,
|
||||
$EVENT INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
|
||||
val CREATE_INDEXES = arrayOf(
|
||||
"CREATE INDEX call_call_id_index ON $TABLE_NAME ($CALL_ID)",
|
||||
"CREATE INDEX call_message_id_index ON $TABLE_NAME ($MESSAGE_ID)"
|
||||
)
|
||||
}
|
||||
|
||||
fun insertCall(callId: Long, timestamp: Long, peer: RecipientId, type: Type, direction: Direction, event: Event) {
|
||||
val messageType: Long = Call.getMessageType(type, direction, event)
|
||||
|
||||
writableDatabase.withinTransaction {
|
||||
val result = SignalDatabase.messages.insertCallLog(peer, messageType, timestamp)
|
||||
|
||||
val values = contentValuesOf(
|
||||
CALL_ID to callId,
|
||||
MESSAGE_ID to result.messageId,
|
||||
PEER to peer.serialize(),
|
||||
TYPE to Type.serialize(type),
|
||||
DIRECTION to Direction.serialize(direction),
|
||||
EVENT to Event.serialize(event)
|
||||
)
|
||||
|
||||
writableDatabase.insert(TABLE_NAME, null, values)
|
||||
}
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context)
|
||||
|
||||
Log.i(TAG, "Inserted call: $callId type: $type direction: $direction event:$event")
|
||||
}
|
||||
|
||||
fun updateCall(callId: Long, event: Event): Call? {
|
||||
return writableDatabase.withinTransaction {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(EVENT to Event.serialize(event))
|
||||
.where("$CALL_ID = ?", callId)
|
||||
.run()
|
||||
|
||||
val call = readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$CALL_ID = ?", callId)
|
||||
.run()
|
||||
.readToSingleObject(Call.Deserializer)
|
||||
|
||||
if (call != null) {
|
||||
Log.i(TAG, "Updated call: $callId event: $event")
|
||||
|
||||
SignalDatabase.messages.updateCallLog(call.messageId, call.messageType)
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context)
|
||||
}
|
||||
|
||||
call
|
||||
}
|
||||
}
|
||||
|
||||
fun getCallById(callId: Long): Call? {
|
||||
return readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$CALL_ID = ?", callId)
|
||||
.run()
|
||||
.readToSingleObject(Call.Deserializer)
|
||||
}
|
||||
|
||||
fun getCallByMessageId(messageId: Long): Call? {
|
||||
return readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$MESSAGE_ID = ?", messageId)
|
||||
.run()
|
||||
.readToSingleObject(Call.Deserializer)
|
||||
}
|
||||
|
||||
fun getCalls(messageIds: Collection<Long>): Map<Long, Call> {
|
||||
val calls = mutableMapOf<Long, Call>()
|
||||
val queries = SqlUtil.buildCollectionQuery(MESSAGE_ID, messageIds)
|
||||
|
||||
queries.forEach { query ->
|
||||
val cursor = readableDatabase
|
||||
.select()
|
||||
.from(TABLE_NAME)
|
||||
.where(query.where, query.whereArgs)
|
||||
.run()
|
||||
|
||||
calls.putAll(cursor.readToList { c -> c.requireLong(MESSAGE_ID) to Call.deserialize(c) })
|
||||
}
|
||||
return calls
|
||||
}
|
||||
|
||||
override fun remapRecipient(fromId: RecipientId, toId: RecipientId) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(PEER to toId.serialize())
|
||||
.where("$PEER = ?", fromId)
|
||||
.run()
|
||||
}
|
||||
|
||||
data class Call(
|
||||
val callId: Long,
|
||||
val peer: RecipientId,
|
||||
val type: Type,
|
||||
val direction: Direction,
|
||||
val event: Event,
|
||||
val messageId: Long
|
||||
) {
|
||||
val messageType: Long = getMessageType(type, direction, event)
|
||||
|
||||
companion object Deserializer : Serializer<Call, Cursor> {
|
||||
fun getMessageType(type: Type, direction: Direction, event: Event): Long {
|
||||
return if (direction == Direction.INCOMING && event == Event.MISSED) {
|
||||
if (type == Type.VIDEO_CALL) MmsSmsColumns.Types.MISSED_VIDEO_CALL_TYPE else MmsSmsColumns.Types.MISSED_AUDIO_CALL_TYPE
|
||||
} else if (direction == Direction.INCOMING) {
|
||||
if (type == Type.VIDEO_CALL) MmsSmsColumns.Types.INCOMING_VIDEO_CALL_TYPE else MmsSmsColumns.Types.INCOMING_AUDIO_CALL_TYPE
|
||||
} else {
|
||||
if (type == Type.VIDEO_CALL) MmsSmsColumns.Types.OUTGOING_VIDEO_CALL_TYPE else MmsSmsColumns.Types.OUTGOING_AUDIO_CALL_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(data: Call): Cursor {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun deserialize(data: Cursor): Call {
|
||||
return Call(
|
||||
callId = data.requireLong(CALL_ID),
|
||||
peer = RecipientId.from(data.requireLong(PEER)),
|
||||
type = data.requireObject(TYPE, Type.Serializer),
|
||||
direction = data.requireObject(DIRECTION, Direction.Serializer),
|
||||
event = data.requireObject(EVENT, Event.Serializer),
|
||||
messageId = data.requireLong(MESSAGE_ID)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type(private val code: Int) {
|
||||
AUDIO_CALL(0),
|
||||
VIDEO_CALL(1);
|
||||
|
||||
companion object Serializer : IntSerializer<Type> {
|
||||
override fun serialize(data: Type): Int = data.code
|
||||
|
||||
override fun deserialize(data: Int): Type {
|
||||
return when (data) {
|
||||
AUDIO_CALL.code -> AUDIO_CALL
|
||||
VIDEO_CALL.code -> VIDEO_CALL
|
||||
else -> throw IllegalArgumentException("Unknown type $data")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun from(type: CallEvent.Type): Type? {
|
||||
return when (type) {
|
||||
CallEvent.Type.UNKNOWN_TYPE -> null
|
||||
CallEvent.Type.AUDIO_CALL -> AUDIO_CALL
|
||||
CallEvent.Type.VIDEO_CALL -> VIDEO_CALL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Direction(private val code: Int) {
|
||||
INCOMING(0),
|
||||
OUTGOING(1);
|
||||
|
||||
companion object Serializer : IntSerializer<Direction> {
|
||||
override fun serialize(data: Direction): Int = data.code
|
||||
|
||||
override fun deserialize(data: Int): Direction {
|
||||
return when (data) {
|
||||
INCOMING.code -> INCOMING
|
||||
OUTGOING.code -> OUTGOING
|
||||
else -> throw IllegalArgumentException("Unknown type $data")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun from(direction: CallEvent.Direction): Direction? {
|
||||
return when (direction) {
|
||||
CallEvent.Direction.UNKNOWN_DIRECTION -> null
|
||||
CallEvent.Direction.INCOMING -> INCOMING
|
||||
CallEvent.Direction.OUTGOING -> OUTGOING
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Event(private val code: Int) {
|
||||
ONGOING(0),
|
||||
ACCEPTED(1),
|
||||
NOT_ACCEPTED(2),
|
||||
MISSED(3);
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun from(event: CallEvent.Event): Event? {
|
||||
return when (event) {
|
||||
CallEvent.Event.UNKNOWN_ACTION -> null
|
||||
CallEvent.Event.ACCEPTED -> ACCEPTED
|
||||
CallEvent.Event.NOT_ACCEPTED -> NOT_ACCEPTED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -559,23 +559,12 @@ public class MessageTable extends DatabaseTable implements MmsSmsColumns, Recipi
|
||||
return results;
|
||||
}
|
||||
|
||||
public @NonNull Pair<Long, Long> insertReceivedCall(@NonNull RecipientId address, boolean isVideoOffer) {
|
||||
return insertCallLog(address, isVideoOffer ? Types.INCOMING_VIDEO_CALL_TYPE : Types.INCOMING_AUDIO_CALL_TYPE, false, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public @NonNull Pair<Long, Long> insertOutgoingCall(@NonNull RecipientId address, boolean isVideoOffer) {
|
||||
return insertCallLog(address, isVideoOffer ? Types.OUTGOING_VIDEO_CALL_TYPE : Types.OUTGOING_AUDIO_CALL_TYPE, false, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public @NonNull Pair<Long, Long> insertMissedCall(@NonNull RecipientId address, long timestamp, boolean isVideoOffer) {
|
||||
return insertCallLog(address, isVideoOffer ? Types.MISSED_VIDEO_CALL_TYPE : Types.MISSED_AUDIO_CALL_TYPE, true, timestamp);
|
||||
}
|
||||
|
||||
private @NonNull Pair<Long, Long> insertCallLog(@NonNull RecipientId recipientId, long type, boolean unread, long timestamp) {
|
||||
public @NonNull InsertResult insertCallLog(@NonNull RecipientId recipientId, long type, long timestamp) {
|
||||
boolean unread = Types.isMissedAudioCall(type) || Types.isMissedVideoCall(type);
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
||||
|
||||
ContentValues values = new ContentValues(6);
|
||||
ContentValues values = new ContentValues(7);
|
||||
values.put(RECIPIENT_ID, recipientId.serialize());
|
||||
values.put(RECIPIENT_DEVICE_ID, 1);
|
||||
values.put(DATE_RECEIVED, System.currentTimeMillis());
|
||||
@@ -584,19 +573,40 @@ public class MessageTable extends DatabaseTable implements MmsSmsColumns, Recipi
|
||||
values.put(TYPE, type);
|
||||
values.put(THREAD_ID, threadId);
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
|
||||
long messageId = db.insert(TABLE_NAME, null, values);
|
||||
long messageId = getWritableDatabase().insert(TABLE_NAME, null, values);
|
||||
boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(recipientId).isMuted();
|
||||
|
||||
if (unread) {
|
||||
SignalDatabase.threads().incrementUnread(threadId, 1, 0);
|
||||
}
|
||||
boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && Recipient.resolved(recipientId).isMuted();
|
||||
|
||||
SignalDatabase.threads().update(threadId, !keepThreadArchived);
|
||||
|
||||
notifyConversationListeners(threadId);
|
||||
TrimThreadJob.enqueueAsync(threadId);
|
||||
|
||||
return new Pair<>(messageId, threadId);
|
||||
return new InsertResult(messageId, threadId);
|
||||
}
|
||||
|
||||
public void updateCallLog(long messageId, long type) {
|
||||
boolean unread = Types.isMissedAudioCall(type) || Types.isMissedVideoCall(type);
|
||||
ContentValues values = new ContentValues(2);
|
||||
values.put(TYPE, type);
|
||||
values.put(READ, unread ? 0 : 1);
|
||||
getWritableDatabase().update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(messageId));
|
||||
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
Recipient recipient = SignalDatabase.threads().getRecipientForThreadId(threadId);
|
||||
boolean keepThreadArchived = SignalStore.settings().shouldKeepMutedChatsArchived() && recipient != null && recipient.isMuted();
|
||||
|
||||
if (unread) {
|
||||
SignalDatabase.threads().incrementUnread(threadId, 1, 0);
|
||||
}
|
||||
|
||||
SignalDatabase.threads().update(threadId, !keepThreadArchived);
|
||||
|
||||
notifyConversationListeners(threadId);
|
||||
ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(new MessageId(messageId));
|
||||
}
|
||||
|
||||
public void insertOrUpdateGroupCall(@NonNull RecipientId groupRecipientId,
|
||||
@@ -4179,6 +4189,7 @@ public class MessageTable extends DatabaseTable implements MmsSmsColumns, Recipi
|
||||
message.getStoryType(),
|
||||
message.getParentStoryId(),
|
||||
message.getGiftBadge(),
|
||||
null,
|
||||
null);
|
||||
}
|
||||
}
|
||||
@@ -4367,7 +4378,7 @@ public class MessageTable extends DatabaseTable implements MmsSmsColumns, Recipi
|
||||
networkFailures, subscriptionId, expiresIn, expireStarted,
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, Collections.emptyList(),
|
||||
remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp, messageRanges,
|
||||
storyType, parentStoryId, giftBadge, null);
|
||||
storyType, parentStoryId, giftBadge, null, null);
|
||||
}
|
||||
|
||||
private Set<IdentityKeyMismatch> getMismatchedIdentities(String document) {
|
||||
|
||||
@@ -74,6 +74,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
val cdsTable: CdsTable = CdsTable(context, this)
|
||||
val remoteMegaphoneTable: RemoteMegaphoneTable = RemoteMegaphoneTable(context, this)
|
||||
val pendingPniSignatureMessageTable: PendingPniSignatureMessageTable = PendingPniSignatureMessageTable(context, this)
|
||||
val callTable: CallTable = CallTable(context, this)
|
||||
|
||||
override fun onOpen(db: net.zetetic.database.sqlcipher.SQLiteDatabase) {
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
@@ -109,6 +110,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
db.execSQL(CdsTable.CREATE_TABLE)
|
||||
db.execSQL(RemoteMegaphoneTable.CREATE_TABLE)
|
||||
db.execSQL(PendingPniSignatureMessageTable.CREATE_TABLE)
|
||||
db.execSQL(CallTable.CREATE_TABLE)
|
||||
executeStatements(db, SearchTable.CREATE_TABLE)
|
||||
executeStatements(db, RemappedRecordTables.CREATE_TABLE)
|
||||
executeStatements(db, MessageSendLogTables.CREATE_TABLE)
|
||||
@@ -133,6 +135,7 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
executeStatements(db, StorySendTable.CREATE_INDEXS)
|
||||
executeStatements(db, DistributionListTables.CREATE_INDEXES)
|
||||
executeStatements(db, PendingPniSignatureMessageTable.CREATE_INDEXES)
|
||||
executeStatements(db, CallTable.CREATE_INDEXES)
|
||||
|
||||
executeStatements(db, SearchTable.CREATE_TRIGGERS)
|
||||
executeStatements(db, MessageSendLogTables.CREATE_TRIGGERS)
|
||||
@@ -523,5 +526,10 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
@get:JvmName("remoteMegaphones")
|
||||
val remoteMegaphones: RemoteMegaphoneTable
|
||||
get() = instance!!.remoteMegaphoneTable
|
||||
|
||||
@get:JvmStatic
|
||||
@get:JvmName("calls")
|
||||
val calls: CallTable
|
||||
get() = instance!!.callTable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V166_ThreadAndMessa
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V167_RecreateReactionTriggers
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V168_SingleMessageTableMigration
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V169_EmojiSearchIndexRank
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V170_CallTableMigration
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
@@ -33,7 +34,7 @@ object SignalDatabaseMigrations {
|
||||
|
||||
val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
|
||||
|
||||
const val DATABASE_VERSION = 169
|
||||
const val DATABASE_VERSION = 170
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -120,6 +121,10 @@ object SignalDatabaseMigrations {
|
||||
if (oldVersion < 169) {
|
||||
V169_EmojiSearchIndexRank.migrate(context, db, oldVersion, newVersion)
|
||||
}
|
||||
|
||||
if (oldVersion < 170) {
|
||||
V170_CallTableMigration.migrate(context, db, oldVersion, newVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
@Suppress("ClassName")
|
||||
object V170_CallTableMigration : SignalDatabaseMigration {
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE call (
|
||||
_id INTEGER PRIMARY KEY,
|
||||
call_id INTEGER NOT NULL UNIQUE,
|
||||
message_id INTEGER NOT NULL REFERENCES mms (_id) ON DELETE CASCADE,
|
||||
peer INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
|
||||
type INTEGER NOT NULL,
|
||||
direction INTEGER NOT NULL,
|
||||
event INTEGER NOT NULL
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
db.execSQL("CREATE INDEX call_call_id_index ON call (call_id)")
|
||||
db.execSQL("CREATE INDEX call_message_id_index ON call (message_id)")
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import android.text.SpannableString;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -29,6 +30,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.CallTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Status;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
@@ -39,6 +41,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.payments.Payment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.payments.FormatterOptions;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -47,6 +50,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -60,9 +64,10 @@ import java.util.stream.Collectors;
|
||||
public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
private final static String TAG = Log.tag(MediaMmsMessageRecord.class);
|
||||
|
||||
private final boolean mentionsSelf;
|
||||
private final BodyRangeList messageRanges;
|
||||
private final Payment payment;
|
||||
private final boolean mentionsSelf;
|
||||
private final BodyRangeList messageRanges;
|
||||
private final Payment payment;
|
||||
private final CallTable.Call call;
|
||||
|
||||
public MediaMmsMessageRecord(long id,
|
||||
Recipient conversationRecipient,
|
||||
@@ -97,7 +102,8 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
@NonNull StoryType storyType,
|
||||
@Nullable ParentStoryId parentStoryId,
|
||||
@Nullable GiftBadge giftBadge,
|
||||
@Nullable Payment payment)
|
||||
@Nullable Payment payment,
|
||||
@Nullable CallTable.Call call)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
|
||||
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
@@ -107,6 +113,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
this.mentionsSelf = mentionsSelf;
|
||||
this.messageRanges = messageRanges;
|
||||
this.payment = payment;
|
||||
this.call = call;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -137,6 +144,40 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return super.getDisplayBody(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context, @Nullable Consumer<RecipientId> recipientClickHandler) {
|
||||
if (isCallLog() && call != null) {
|
||||
boolean accepted = call.getEvent() == CallTable.Event.ACCEPTED;
|
||||
String callDateString = getCallDateString(context);
|
||||
|
||||
if (call.getDirection() == CallTable.Direction.OUTGOING) {
|
||||
if (call.getType() == CallTable.Type.AUDIO_CALL) {
|
||||
int updateString = accepted ? R.string.MessageRecord_you_called_date : R.string.MessageRecord_unanswered_audio_call_date;
|
||||
return staticUpdateDescription(context.getString(updateString, callDateString), R.drawable.ic_update_audio_call_outgoing_16);
|
||||
} else {
|
||||
int updateString = accepted ? R.string.MessageRecord_you_called_date : R.string.MessageRecord_unanswered_video_call_date;
|
||||
return staticUpdateDescription(context.getString(updateString, callDateString), R.drawable.ic_update_video_call_outgoing_16);
|
||||
}
|
||||
} else {
|
||||
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
|
||||
boolean isMissed = call.getEvent() == CallTable.Event.MISSED;
|
||||
|
||||
if (accepted) {
|
||||
int icon = isVideoCall ? R.drawable.ic_update_video_call_incoming_16 : R.drawable.ic_update_audio_call_incoming_16;
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_called_you_date, r.getDisplayName(context), callDateString), icon);
|
||||
} else if (isMissed) {
|
||||
return isVideoCall ? staticUpdateDescription(context.getString(R.string.MessageRecord_missed_video_call_date, callDateString), R.drawable.ic_update_video_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red))
|
||||
: staticUpdateDescription(context.getString(R.string.MessageRecord_missed_audio_call_date, callDateString), R.drawable.ic_update_audio_call_missed_16, ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red));
|
||||
} else {
|
||||
return isVideoCall ? fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_declined_video_call_date, callDateString), R.drawable.ic_update_video_call_incoming_16)
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_declined_audio_call_date, callDateString), R.drawable.ic_update_audio_call_incoming_16);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.getUpdateDisplayBody(context, recipientClickHandler);
|
||||
}
|
||||
|
||||
|
||||
public @Nullable BodyRangeList getMessageRanges() {
|
||||
return messageRanges;
|
||||
}
|
||||
@@ -155,18 +196,22 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return payment;
|
||||
}
|
||||
|
||||
public @Nullable CallTable.Call getCall() {
|
||||
return call;
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withoutQuote() {
|
||||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withAttachments(@NonNull Context context, @NonNull List<DatabaseAttachment> attachments) {
|
||||
@@ -187,14 +232,22 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), slideDeck,
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withPayment(@NonNull Payment payment) {
|
||||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment);
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall());
|
||||
}
|
||||
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withCall(@Nullable CallTable.Call call) {
|
||||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call);
|
||||
}
|
||||
|
||||
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
|
||||
|
||||
@@ -337,29 +337,29 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return null;
|
||||
}
|
||||
|
||||
private @NonNull String getCallDateString(@NonNull Context context) {
|
||||
protected @NonNull String getCallDateString(@NonNull Context context) {
|
||||
return DateUtils.getSimpleRelativeTimeSpanString(context, Locale.getDefault(), getDateSent());
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient,
|
||||
@NonNull Function<Recipient, String> stringGenerator,
|
||||
@DrawableRes int iconResource)
|
||||
protected static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient,
|
||||
@NonNull Function<Recipient, String> stringGenerator,
|
||||
@DrawableRes int iconResource)
|
||||
{
|
||||
return UpdateDescription.mentioning(Collections.singletonList(recipient.getServiceId().orElse(ServiceId.UNKNOWN)),
|
||||
() -> new SpannableString(stringGenerator.apply(recipient.resolve())),
|
||||
iconResource);
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
|
||||
@DrawableRes int iconResource)
|
||||
protected static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
|
||||
@DrawableRes int iconResource)
|
||||
{
|
||||
return UpdateDescription.staticDescription(string, iconResource);
|
||||
}
|
||||
|
||||
private static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
|
||||
@DrawableRes int iconResource,
|
||||
@ColorInt int lightTint,
|
||||
@ColorInt int darkTint)
|
||||
protected static @NonNull UpdateDescription staticUpdateDescription(@NonNull String string,
|
||||
@DrawableRes int iconResource,
|
||||
@ColorInt int lightTint,
|
||||
@ColorInt int darkTint)
|
||||
{
|
||||
return UpdateDescription.staticDescription(string, iconResource, lightTint, darkTint);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
import org.thoughtcrime.securesms.database.CallTable;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.GroupTable.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable;
|
||||
@@ -169,6 +170,7 @@ import org.whispersystems.signalservice.api.payments.Money;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
@@ -350,6 +352,7 @@ public final class MessageContentProcessor {
|
||||
else if (syncMessage.getOutgoingPaymentMessage().isPresent()) handleSynchronizeOutgoingPayment(content, syncMessage.getOutgoingPaymentMessage().get());
|
||||
else if (syncMessage.getKeys().isPresent()) handleSynchronizeKeys(syncMessage.getKeys().get(), content.getTimestamp());
|
||||
else if (syncMessage.getContacts().isPresent()) handleSynchronizeContacts(syncMessage.getContacts().get(), content.getTimestamp());
|
||||
else if (syncMessage.getCallEvent().isPresent()) handleSynchronizeCallEvent(syncMessage.getCallEvent().get(), content.getTimestamp());
|
||||
else warn(String.valueOf(content.getTimestamp()), "Contains no known sync types...");
|
||||
} else if (content.getCallMessage().isPresent()) {
|
||||
log(String.valueOf(content.getTimestamp()), "Got call message...");
|
||||
@@ -627,7 +630,7 @@ public final class MessageContentProcessor {
|
||||
@NonNull AnswerMessage message,
|
||||
@NonNull Recipient senderRecipient)
|
||||
{
|
||||
log(String.valueOf(content), "handleCallAnswerMessage...");
|
||||
log(content.getTimestamp(), "handleCallAnswerMessage...");
|
||||
RemotePeer remotePeer = new RemotePeer(senderRecipient.getId(), new CallId(message.getId()));
|
||||
byte[] remoteIdentityKey = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecord(senderRecipient.getId()).map(record -> record.getIdentityKey().serialize()).get();
|
||||
|
||||
@@ -641,7 +644,7 @@ public final class MessageContentProcessor {
|
||||
@NonNull List<IceUpdateMessage> messages,
|
||||
@NonNull Recipient senderRecipient)
|
||||
{
|
||||
log(String.valueOf(content), "handleCallIceUpdateMessage... " + messages.size());
|
||||
log(content.getTimestamp(), "handleCallIceUpdateMessage... " + messages.size());
|
||||
|
||||
List<byte[]> iceCandidates = new ArrayList<>(messages.size());
|
||||
long callId = -1;
|
||||
@@ -663,7 +666,7 @@ public final class MessageContentProcessor {
|
||||
@NonNull Optional<Long> smsMessageId,
|
||||
@NonNull Recipient senderRecipient)
|
||||
{
|
||||
log(String.valueOf(content), "handleCallHangupMessage");
|
||||
log(content.getTimestamp(), "handleCallHangupMessage");
|
||||
if (smsMessageId.isPresent()) {
|
||||
SignalDatabase.messages().markAsMissedCall(smsMessageId.get(), false);
|
||||
} else {
|
||||
@@ -1228,6 +1231,45 @@ public final class MessageContentProcessor {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactSyncJob(contactsAttachment));
|
||||
}
|
||||
|
||||
private void handleSynchronizeCallEvent(@NonNull SyncMessage.CallEvent callEvent, long envelopeTimestamp) {
|
||||
if (!callEvent.hasId()) {
|
||||
log(envelopeTimestamp, "Synchronize call event missing call id, ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
long callId = callEvent.getId();
|
||||
long timestamp = callEvent.getTimestamp();
|
||||
CallTable.Type type = CallTable.Type.from(callEvent.getType());
|
||||
CallTable.Direction direction = CallTable.Direction.from(callEvent.getDirection());
|
||||
CallTable.Event event = CallTable.Event.from(callEvent.getEvent());
|
||||
|
||||
if (timestamp == 0 || type == null || direction == null || event == null || !callEvent.hasPeerUuid()) {
|
||||
warn(envelopeTimestamp, "Call event sync message is not valid, ignoring. timestamp: " + timestamp + " type: " + type + " direction: " + direction + " event: " + event + " hasPeer: " + callEvent.hasPeerUuid());
|
||||
return;
|
||||
}
|
||||
|
||||
ServiceId serviceId = ServiceId.fromByteString(callEvent.getPeerUuid());
|
||||
RecipientId recipientId = RecipientId.from(serviceId);
|
||||
|
||||
log(envelopeTimestamp, "Synchronize call event call: " + callId);
|
||||
|
||||
CallTable.Call call = SignalDatabase.calls().getCallById(callId);
|
||||
if (call != null) {
|
||||
boolean typeMismatch = call.getType() != type;
|
||||
boolean directionMismatch = call.getDirection() != direction;
|
||||
boolean eventDowngrade = call.getEvent() == CallTable.Event.ACCEPTED && event != CallTable.Event.ACCEPTED;
|
||||
boolean peerMismatch = !call.getPeer().equals(recipientId);
|
||||
|
||||
if (typeMismatch || directionMismatch || eventDowngrade || peerMismatch) {
|
||||
warn(envelopeTimestamp, "Call event sync message is not valid for existing call record, ignoring. type: " + type + " direction: " + direction + " event: " + event + " peerMismatch: " + peerMismatch);
|
||||
} else {
|
||||
SignalDatabase.calls().updateCall(callId, event);
|
||||
}
|
||||
} else {
|
||||
SignalDatabase.calls().insertCall(callId, timestamp, recipientId, type, direction, event);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
|
||||
@NonNull SentTranscriptMessage message,
|
||||
@NonNull Recipient senderRecipient)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.thoughtcrime.securesms.service.webrtc
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.ringrtc.RemotePeer
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage.CallEvent
|
||||
|
||||
/**
|
||||
* Helper for creating call event sync messages.
|
||||
*/
|
||||
object CallEventSyncMessageUtil {
|
||||
@JvmStatic
|
||||
fun createAcceptedSyncMessage(remotePeer: RemotePeer, timestamp: Long, isOutgoing: Boolean, isVideoCall: Boolean): CallEvent {
|
||||
return CallEvent
|
||||
.newBuilder()
|
||||
.setPeerUuid(Recipient.resolved(remotePeer.id).requireServiceId().toByteString())
|
||||
.setId(remotePeer.callId.longValue())
|
||||
.setTimestamp(timestamp)
|
||||
.setType(if (isVideoCall) CallEvent.Type.VIDEO_CALL else CallEvent.Type.AUDIO_CALL)
|
||||
.setDirection(if (isOutgoing) CallEvent.Direction.OUTGOING else CallEvent.Direction.INCOMING)
|
||||
.setEvent(CallEvent.Event.ACCEPTED)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createNotAcceptedSyncMessage(remotePeer: RemotePeer, timestamp: Long, isOutgoing: Boolean, isVideoCall: Boolean): CallEvent {
|
||||
return CallEvent
|
||||
.newBuilder()
|
||||
.setPeerUuid(Recipient.resolved(remotePeer.id).requireServiceId().toByteString())
|
||||
.setId(remotePeer.callId.longValue())
|
||||
.setTimestamp(timestamp)
|
||||
.setType(if (isVideoCall) CallEvent.Type.VIDEO_CALL else CallEvent.Type.AUDIO_CALL)
|
||||
.setDirection(if (isOutgoing) CallEvent.Direction.OUTGOING else CallEvent.Direction.INCOMING)
|
||||
.setEvent(CallEvent.Event.NOT_ACCEPTED)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,12 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor {
|
||||
|
||||
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
|
||||
|
||||
webRtcInteractor.sendAcceptedCallEventSyncMessage(
|
||||
activePeer,
|
||||
currentState.getCallInfoState().getCallState() == WebRtcViewModel.State.CALL_RINGING,
|
||||
currentState.getCallSetupState(activePeer).isAcceptWithVideo() || currentState.getLocalDeviceState().getCameraState().isEnabled()
|
||||
);
|
||||
|
||||
ApplicationDependencies.getAppForegroundObserver().removeListener(webRtcInteractor.getForegroundListener());
|
||||
webRtcInteractor.startAudioCommunication();
|
||||
webRtcInteractor.activateCall(activePeer.getId());
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.database.CallTable;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -130,8 +131,6 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
Log.i(TAG, "handleAcceptCall(): call_id: " + activePeer.getCallId());
|
||||
|
||||
SignalDatabase.messages().insertReceivedCall(activePeer.getId(), currentState.getCallSetupState(activePeer).isRemoteVideoOffer());
|
||||
|
||||
currentState = currentState.builder()
|
||||
.changeCallSetupState(activePeer.getCallId())
|
||||
.acceptWithVideo(answerWithVideo)
|
||||
@@ -156,10 +155,13 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
Log.i(TAG, "handleDenyCall():");
|
||||
|
||||
webRtcInteractor.sendNotAcceptedCallEventSyncMessage(activePeer,
|
||||
false,
|
||||
currentState.getCallSetupState(activePeer).isAcceptWithVideo() || currentState.getLocalDeviceState().getCameraState().isEnabled());
|
||||
|
||||
try {
|
||||
webRtcInteractor.rejectIncomingCall(activePeer.getId());
|
||||
webRtcInteractor.getCallManager().hangup();
|
||||
SignalDatabase.messages().insertMissedCall(activePeer.getId(), System.currentTimeMillis(), currentState.getCallSetupState(activePeer).isRemoteVideoOffer());
|
||||
return terminate(currentState, activePeer);
|
||||
} catch (CallException e) {
|
||||
return callFailure(currentState, "hangup() failed: ", e);
|
||||
@@ -173,6 +175,14 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
Recipient recipient = remotePeer.getRecipient();
|
||||
|
||||
activePeer.localRinging();
|
||||
|
||||
SignalDatabase.calls().insertCall(remotePeer.getCallId().longValue(),
|
||||
System.currentTimeMillis(),
|
||||
remotePeer.getId(),
|
||||
currentState.getCallSetupState(activePeer).isRemoteVideoOffer() ? CallTable.Type.VIDEO_CALL : CallTable.Type.AUDIO_CALL,
|
||||
CallTable.Direction.INCOMING,
|
||||
CallTable.Event.ONGOING);
|
||||
|
||||
webRtcInteractor.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
|
||||
|
||||
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext(), recipient);
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper;
|
||||
import org.thoughtcrime.securesms.database.CallTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
@@ -83,7 +84,13 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
}
|
||||
|
||||
RecipientUtil.setAndSendUniversalExpireTimerIfNecessary(context, Recipient.resolved(remotePeer.getId()), SignalDatabase.threads().getThreadIdIfExistsFor(remotePeer.getId()));
|
||||
SignalDatabase.messages().insertOutgoingCall(remotePeer.getId(), isVideoCall);
|
||||
|
||||
SignalDatabase.calls().insertCall(remotePeer.getCallId().longValue(),
|
||||
System.currentTimeMillis(),
|
||||
remotePeer.getId(),
|
||||
isVideoCall ? CallTable.Type.VIDEO_CALL : CallTable.Type.AUDIO_CALL,
|
||||
CallTable.Direction.OUTGOING,
|
||||
CallTable.Event.ONGOING);
|
||||
|
||||
EglBaseWrapper.replaceHolder(EglBaseWrapper.OUTGOING_PLACEHOLDER, remotePeer.getCallId().longValue());
|
||||
|
||||
@@ -236,6 +243,13 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
|
||||
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
|
||||
if (activePeer != null) {
|
||||
webRtcInteractor.sendNotAcceptedCallEventSyncMessage(activePeer,
|
||||
true,
|
||||
currentState.getCallSetupState(activePeer).isAcceptWithVideo() || currentState.getLocalDeviceState().getCameraState().isEnabled());
|
||||
}
|
||||
|
||||
return activeCallDelegate.handleLocalHangup(currentState);
|
||||
}
|
||||
|
||||
@@ -246,6 +260,19 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleEndedRemote(@NonNull WebRtcServiceState currentState, @NonNull CallManager.CallEvent endedRemoteEvent, @NonNull RemotePeer remotePeer) {
|
||||
RemotePeer activePeer = currentState.getCallInfoState().getActivePeer();
|
||||
if (activePeer != null &&
|
||||
(endedRemoteEvent == CallManager.CallEvent.ENDED_REMOTE_HANGUP ||
|
||||
endedRemoteEvent == CallManager.CallEvent.ENDED_REMOTE_HANGUP_NEED_PERMISSION ||
|
||||
endedRemoteEvent == CallManager.CallEvent.ENDED_REMOTE_BUSY ||
|
||||
endedRemoteEvent == CallManager.CallEvent.ENDED_TIMEOUT ||
|
||||
endedRemoteEvent == CallManager.CallEvent.ENDED_REMOTE_GLARE))
|
||||
{
|
||||
webRtcInteractor.sendNotAcceptedCallEventSyncMessage(activePeer,
|
||||
true,
|
||||
currentState.getCallSetupState(activePeer).isAcceptWithVideo() || currentState.getLocalDeviceState().getCameraState().isEnabled());
|
||||
}
|
||||
|
||||
return activeCallDelegate.handleEndedRemote(currentState, endedRemoteEvent, remotePeer);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.signal.storageservice.protos.groups.GroupExternalCredential;
|
||||
import org.thoughtcrime.securesms.WebRtcCallActivity;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.CallTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
|
||||
@@ -66,8 +67,10 @@ import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
|
||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
@@ -521,7 +524,7 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
|
||||
Log.i(TAG, "Ignoring event: " + event);
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unexpected event: " + event.toString());
|
||||
throw new AssertionError("Unexpected event: " + event);
|
||||
}
|
||||
|
||||
return s;
|
||||
@@ -825,18 +828,28 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
|
||||
});
|
||||
}
|
||||
|
||||
public void insertMissedCall(@NonNull RemotePeer remotePeer, boolean signal, long timestamp, boolean isVideoOffer) {
|
||||
Pair<Long, Long> messageAndThreadId = SignalDatabase.messages().insertMissedCall(remotePeer.getId(), timestamp, isVideoOffer);
|
||||
public void insertMissedCall(@NonNull RemotePeer remotePeer, long timestamp, boolean isVideoOffer) {
|
||||
CallTable.Call call = SignalDatabase.calls()
|
||||
.updateCall(remotePeer.getCallId().longValue(), CallTable.Event.MISSED);
|
||||
|
||||
ApplicationDependencies.getMessageNotifier()
|
||||
.updateNotification(context, ConversationId.forConversation(messageAndThreadId.second()), signal);
|
||||
if (call == null) {
|
||||
CallTable.Type type = isVideoOffer ? CallTable.Type.VIDEO_CALL : CallTable.Type.AUDIO_CALL;
|
||||
|
||||
SignalDatabase.calls()
|
||||
.insertCall(remotePeer.getCallId().longValue(), timestamp, remotePeer.getId(), type, CallTable.Direction.INCOMING, CallTable.Event.MISSED);
|
||||
}
|
||||
}
|
||||
|
||||
public void insertReceivedCall(@NonNull RemotePeer remotePeer, boolean signal, boolean isVideoOffer) {
|
||||
Pair<Long, Long> messageAndThreadId = SignalDatabase.messages().insertReceivedCall(remotePeer.getId(), isVideoOffer);
|
||||
public void insertReceivedCall(@NonNull RemotePeer remotePeer, boolean isVideoOffer) {
|
||||
CallTable.Call call = SignalDatabase.calls()
|
||||
.updateCall(remotePeer.getCallId().longValue(), CallTable.Event.ACCEPTED);
|
||||
|
||||
ApplicationDependencies.getMessageNotifier()
|
||||
.updateNotification(context, ConversationId.forConversation(messageAndThreadId.second()), signal);
|
||||
if (call == null) {
|
||||
CallTable.Type type = isVideoOffer ? CallTable.Type.VIDEO_CALL : CallTable.Type.AUDIO_CALL;
|
||||
|
||||
SignalDatabase.calls()
|
||||
.insertCall(remotePeer.getCallId().longValue(), System.currentTimeMillis(), remotePeer.getId(), type, CallTable.Direction.INCOMING, CallTable.Event.ACCEPTED);
|
||||
}
|
||||
}
|
||||
|
||||
public void retrieveTurnServers(@NonNull RemotePeer remotePeer) {
|
||||
@@ -919,6 +932,40 @@ private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEph
|
||||
});
|
||||
}
|
||||
|
||||
public void sendAcceptedCallEventSyncMessage(@NonNull RemotePeer remotePeer, boolean isOutgoing, boolean isVideoCall) {
|
||||
SignalDatabase
|
||||
.calls()
|
||||
.updateCall(remotePeer.getCallId().longValue(), CallTable.Event.ACCEPTED);
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
networkExecutor.execute(() -> {
|
||||
try {
|
||||
SyncMessage.CallEvent callEvent = CallEventSyncMessageUtil.createAcceptedSyncMessage(remotePeer, System.currentTimeMillis(), isOutgoing, isVideoCall);
|
||||
ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(callEvent), Optional.empty());
|
||||
} catch (IOException | UntrustedIdentityException e) {
|
||||
Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void sendNotAcceptedCallEventSyncMessage(@NonNull RemotePeer remotePeer, boolean isOutgoing, boolean isVideoCall) {
|
||||
SignalDatabase
|
||||
.calls()
|
||||
.updateCall(remotePeer.getCallId().longValue(), CallTable.Event.NOT_ACCEPTED);
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
networkExecutor.execute(() -> {
|
||||
try {
|
||||
SyncMessage.CallEvent callEvent = CallEventSyncMessageUtil.createNotAcceptedSyncMessage(remotePeer, System.currentTimeMillis(), isOutgoing, isVideoCall);
|
||||
ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(SignalServiceSyncMessage.forCallEvent(callEvent), Optional.empty());
|
||||
} catch (IOException | UntrustedIdentityException e) {
|
||||
Log.w(TAG, "Unable to send call event sync message for " + remotePeer.getCallId().longValue(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void processSendMessageFailureWithChangeDetection(@NonNull RemotePeer remotePeer,
|
||||
@NonNull ProcessAction failureProcessAction)
|
||||
{
|
||||
|
||||
@@ -109,11 +109,11 @@ public class WebRtcInteractor {
|
||||
}
|
||||
|
||||
void insertMissedCall(@NonNull RemotePeer remotePeer, long timestamp, boolean isVideoOffer) {
|
||||
signalCallManager.insertMissedCall(remotePeer, true, timestamp, isVideoOffer);
|
||||
signalCallManager.insertMissedCall(remotePeer, timestamp, isVideoOffer);
|
||||
}
|
||||
|
||||
void insertReceivedCall(@NonNull RemotePeer remotePeer, boolean isVideoOffer) {
|
||||
signalCallManager.insertReceivedCall(remotePeer, true, isVideoOffer);
|
||||
signalCallManager.insertReceivedCall(remotePeer, isVideoOffer);
|
||||
}
|
||||
|
||||
boolean startWebRtcCallActivityIfPossible() {
|
||||
@@ -187,4 +187,12 @@ public class WebRtcInteractor {
|
||||
public void requestGroupMembershipProof(GroupId.V2 groupId, int groupCallHashCode) {
|
||||
signalCallManager.requestGroupMembershipToken(groupId, groupCallHashCode);
|
||||
}
|
||||
|
||||
public void sendAcceptedCallEventSyncMessage(@NonNull RemotePeer remotePeer, boolean isOutgoing, boolean isVideoCall) {
|
||||
signalCallManager.sendAcceptedCallEventSyncMessage(remotePeer, isOutgoing, isVideoCall);
|
||||
}
|
||||
|
||||
public void sendNotAcceptedCallEventSyncMessage(@NonNull RemotePeer remotePeer, boolean isOutgoing, boolean isVideoCall) {
|
||||
signalCallManager.sendNotAcceptedCallEventSyncMessage(remotePeer, isOutgoing, isVideoCall);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user