Update call strings to align with new designs.

This commit is contained in:
Alex Hart 2024-04-15 13:56:20 -03:00 committed by Greyson Parrelli
parent a83abaca1d
commit 1b7784b01f
17 changed files with 535 additions and 124 deletions

View file

@ -9,6 +9,7 @@ import android.Manifest
import android.app.UiAutomation import android.app.UiAutomation
import android.os.Environment import android.os.Environment
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import io.mockk.InternalPlatformDsl.toArray
import okio.ByteString.Companion.toByteString import okio.ByteString.Companion.toByteString
import org.junit.Assert import org.junit.Assert
import org.junit.Before import org.junit.Before
@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.Call import org.thoughtcrime.securesms.backup.v2.proto.Call
import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Chat import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
@ -32,6 +34,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.Frame import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.proto.Group import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Quote import org.thoughtcrime.securesms.backup.v2.proto.Quote
@ -668,12 +672,62 @@ class ImportExportTest {
) )
} }
var sentTime = 0L
val individualCallChatItems = individualCalls.map { call ->
ChatItem(
chatId = 1,
authorId = selfRecipient.id,
dateSent = sentTime++,
sms = false,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = sentTime + 1,
dateServerSent = sentTime,
read = true,
sealedSender = true
),
updateMessage = ChatUpdateMessage(
callingMessage = CallChatUpdate(
callMessage = IndividualCallChatUpdate(
type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL
)
)
)
)
}.toTypedArray()
val startedAci = TestRecipientUtils.nextAci().toByteString()
val groupCallChatItems = groupCalls.map { call ->
ChatItem(
chatId = 1,
authorId = selfRecipient.id,
dateSent = sentTime++,
sms = false,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = sentTime + 1,
dateServerSent = sentTime,
read = true,
sealedSender = true
),
updateMessage = ChatUpdateMessage(
callingMessage = CallChatUpdate(
groupCall = GroupCallChatUpdate(
startedCallAci = startedAci,
startedCallTimestamp = 0,
endedCallTimestamp = 0,
localUserJoined = GroupCallChatUpdate.LocalUserJoined.JOINED,
inCallAcis = emptyList()
)
)
)
)
}.toTypedArray()
importExport( importExport(
*standardFrames, *standardFrames,
Recipient( Recipient(
id = 3, id = 3,
contact = Contact( contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(), aci = startedAci,
pni = TestRecipientUtils.nextPni().toByteString(), pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01", username = "cool.01",
e164 = 141255501234, e164 = 141255501234,
@ -698,8 +752,21 @@ class ImportExportTest {
name = "Cool test group" name = "Cool test group"
) )
), ),
Chat(
id = 1,
recipientId = 3,
archived = true,
pinnedOrder = 1,
expirationTimerMs = 1.days.inWholeMilliseconds,
muteUntilMs = System.currentTimeMillis(),
markedUnread = true,
dontNotifyForMentionsIfMuted = true,
wallpaper = null
),
*individualCalls.toArray(), *individualCalls.toArray(),
*groupCalls.toArray() *groupCalls.toArray(),
*individualCallChatItems,
*groupCallChatItems
) )
} }

View file

@ -200,6 +200,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
} }
} }
MessageTypes.isCallLog(record.type) -> { MessageTypes.isCallLog(record.type) -> {
builder.sms = false
val call = calls.getCallByMessageId(record.id) val call = calls.getCallByMessageId(record.id)
if (call != null) { if (call != null) {
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callId = call.callId)) builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callId = call.callId))
@ -232,12 +233,23 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
.withoutNulls() .withoutNulls()
.map { obj: UUID? -> ACI.from(obj!!).toByteString() } .map { obj: UUID? -> ACI.from(obj!!).toByteString() }
.toList() .toList()
val localUserJoined: GroupCallChatUpdate.LocalUserJoined = if (groupCallUpdateDetails.localUserJoined) {
GroupCallChatUpdate.LocalUserJoined.JOINED
} else if (groupCallUpdateDetails.endedCallTimestamp == 0L) {
GroupCallChatUpdate.LocalUserJoined.UNKNOWN
} else {
GroupCallChatUpdate.LocalUserJoined.DID_NOT_JOIN
}
builder.updateMessage = ChatUpdateMessage( builder.updateMessage = ChatUpdateMessage(
callingMessage = CallChatUpdate( callingMessage = CallChatUpdate(
groupCall = GroupCallChatUpdate( groupCall = GroupCallChatUpdate(
startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(), startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(),
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp, startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
inCallAcis = joinedMembers inCallAcis = joinedMembers,
localUserJoined = localUserJoined,
endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp
) )
) )
) )

View file

@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
import org.thoughtcrime.securesms.database.documents.NetworkFailure import org.thoughtcrime.securesms.database.documents.NetworkFailure
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
import org.thoughtcrime.securesms.database.model.Mention import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
@ -460,6 +461,10 @@ class ChatItemImportInserter(
IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags
} }
} }
updateMessage.callingMessage.groupCall != null -> {
typeFlags = MessageTypes.GROUP_CALL_TYPE
this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.callingMessage.groupCall))
}
} }
// Calls don't use the incoming/outgoing flags, so we overwrite the flags here // Calls don't use the incoming/outgoing flags, so we overwrite the flags here
this.put(MessageTable.TYPE, typeFlags) this.put(MessageTable.TYPE, typeFlags)

View file

@ -305,7 +305,7 @@ class CallLogAdapter(
val color = ContextCompat.getColor( val color = ContextCompat.getColor(
context, context,
if (call.record.event.isMissedCall()) { if (call.record.isDisplayedAsMissedCallInUi) {
R.color.signal_colorError R.color.signal_colorError
} else { } else {
R.color.signal_colorOnSurfaceVariant R.color.signal_colorOnSurfaceVariant
@ -371,11 +371,11 @@ class CallLogAdapter(
private fun getCallStateDrawableRes(call: CallTable.Call): Int { private fun getCallStateDrawableRes(call: CallTable.Call): Int {
return when (call.messageType) { return when (call.messageType) {
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_compact_16 MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_compact_16
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_compact_16 MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) R.drawable.symbol_missed_incoming_compact_16 else R.drawable.symbol_arrow_downleft_compact_16
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16 MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16
MessageTypes.GROUP_CALL_TYPE -> when { MessageTypes.GROUP_CALL_TYPE -> when {
call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16 call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16
call.event.isMissedCall() -> R.drawable.symbol_missed_incoming_compact_16 call.isDisplayedAsMissedCallInUi -> R.drawable.symbol_missed_incoming_compact_16
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16 call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16 call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16 call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16
@ -389,23 +389,19 @@ class CallLogAdapter(
@StringRes @StringRes
private fun getCallStateStringRes(call: CallTable.Call): Int { private fun getCallStateStringRes(call: CallTable.Call): Int {
return when (call.messageType) { return when (call.messageType) {
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__missed_notification_profile
MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__missed_notification_profile
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__incoming
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__incoming
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__outgoing MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
MessageTypes.GROUP_CALL_TYPE -> when { MessageTypes.GROUP_CALL_TYPE -> when {
call.type == CallTable.Type.AD_HOC_CALL -> R.string.CallLogAdapter__call_link call.type == CallTable.Type.AD_HOC_CALL -> R.string.CallLogAdapter__call_link
call.event == CallTable.Event.MISSED -> R.string.CallLogAdapter__missed
call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallLogAdapter__missed_notification_profile call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallLogAdapter__missed_notification_profile
call.isDisplayedAsMissedCallInUi -> R.string.CallLogAdapter__missed
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
call.direction == CallTable.Direction.INCOMING -> R.string.CallLogAdapter__incoming call.direction == CallTable.Direction.INCOMING -> R.string.CallLogAdapter__incoming
call.direction == CallTable.Direction.OUTGOING -> R.string.CallLogAdapter__outgoing call.direction == CallTable.Direction.OUTGOING -> R.string.CallLogAdapter__outgoing
else -> throw AssertionError() else -> throw AssertionError()
} }
else -> if (call.isDisplayedAsMissedCallInUi) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__incoming
else -> error("Unexpected type ${call.messageType}")
} }
} }
} }

View file

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.settings.conversation.preferences package org.thoughtcrime.securesms.components.settings.conversation.preferences
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.CallTable import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTypes import org.thoughtcrime.securesms.database.MessageTypes
@ -46,10 +47,10 @@ object CallPreference {
private fun getCallIcon(call: CallTable.Call): Int { private fun getCallIcon(call: CallTable.Call): Int {
return when (call.messageType) { return when (call.messageType) {
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_24 MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_24
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_24 MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) R.drawable.symbol_missed_incoming_24 else R.drawable.symbol_arrow_downleft_24
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_24 MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_24
MessageTypes.GROUP_CALL_TYPE -> when { MessageTypes.GROUP_CALL_TYPE -> when {
call.event.isMissedCall() -> R.drawable.symbol_missed_incoming_24 call.isDisplayedAsMissedCallInUi -> R.drawable.symbol_missed_incoming_24
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_24 call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_24
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_24 call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_24
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_24 call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_24
@ -61,15 +62,14 @@ object CallPreference {
private fun getCallType(call: CallTable.Call): String { private fun getCallType(call: CallTable.Call): String {
val id = when (call.messageType) { val id = when (call.messageType) {
MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.MessageRecord_missed_voice_call else R.string.MessageRecord_missed_voice_call_notification_profile MessageTypes.MISSED_AUDIO_CALL_TYPE -> getMissedCallString(false, call.event)
MessageTypes.MISSED_VIDEO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.MessageRecord_missed_video_call else R.string.MessageRecord_missed_video_call_notification_profile MessageTypes.MISSED_VIDEO_CALL_TYPE -> getMissedCallString(true, call.event)
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.MessageRecord_incoming_voice_call MessageTypes.INCOMING_AUDIO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) getMissedCallString(false, call.event) else R.string.MessageRecord_incoming_voice_call
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.MessageRecord_incoming_video_call MessageTypes.INCOMING_VIDEO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) getMissedCallString(true, call.event) else R.string.MessageRecord_incoming_video_call
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.MessageRecord_outgoing_voice_call MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.MessageRecord_outgoing_voice_call
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.MessageRecord_outgoing_video_call MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.MessageRecord_outgoing_video_call
MessageTypes.GROUP_CALL_TYPE -> when { MessageTypes.GROUP_CALL_TYPE -> when {
call.event == CallTable.Event.MISSED -> R.string.CallPreference__missed_group_call call.isDisplayedAsMissedCallInUi -> if (call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE) R.string.CallPreference__missed_group_call_notification_profile else R.string.CallPreference__missed_group_call
call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallPreference__missed_group_call_notification_profile
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
call.direction == CallTable.Direction.INCOMING -> R.string.CallPreference__incoming_group_call call.direction == CallTable.Direction.INCOMING -> R.string.CallPreference__incoming_group_call
call.direction == CallTable.Direction.OUTGOING -> R.string.CallPreference__outgoing_group_call call.direction == CallTable.Direction.OUTGOING -> R.string.CallPreference__outgoing_group_call
@ -81,6 +81,23 @@ object CallPreference {
return context.getString(id) return context.getString(id)
} }
@StringRes
private fun getMissedCallString(isVideo: Boolean, callEvent: CallTable.Event): Int {
return if (callEvent == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
if (isVideo) {
R.string.MessageRecord_missed_video_call_notification_profile
} else {
R.string.MessageRecord_missed_voice_call_notification_profile
}
} else {
if (isVideo) {
R.string.MessageRecord_missed_video_call
} else {
R.string.MessageRecord_missed_voice_call
}
}
}
private fun getCallTime(messageRecord: MessageRecord): String { private fun getCallTime(messageRecord: MessageRecord): String {
return DateUtils.getOnlyTimeString(context, messageRecord.timestamp) return DateUtils.getOnlyTimeString(context, messageRecord.timestamp)
} }

View file

@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage; import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.UpdateDescription; import org.thoughtcrime.securesms.database.model.UpdateDescription;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.groups.LiveGroup; import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
@ -447,11 +448,14 @@ public final class ConversationUpdateItem extends FrameLayout
} }
}); });
} else if (conversationMessage.getMessageRecord().isGroupCall()) { } else if (conversationMessage.getMessageRecord().isGroupCall()) {
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody());
boolean isRingingOnLocalDevice = groupCallUpdateDetails.isRingingOnLocalDevice;
boolean endedRecently = GroupCallUpdateDetailsUtil.checkCallEndedRecently(groupCallUpdateDetails);
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true); UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<ACI> acis = updateDescription.getMentioned(); Collection<ACI> acis = updateDescription.getMentioned();
int text = 0; int text = 0;
if (Util.hasItems(acis)) { if (Util.hasItems(acis) || isRingingOnLocalDevice) {
if (acis.contains(SignalStore.account().requireAci())) { if (acis.contains(SignalStore.account().requireAci())) {
text = R.string.ConversationUpdateItem_return_to_call; text = R.string.ConversationUpdateItem_return_to_call;
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).isCallFull) { } else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).isCallFull) {
@ -459,6 +463,8 @@ public final class ConversationUpdateItem extends FrameLayout
} else { } else {
text = R.string.ConversationUpdateItem_join_call; text = R.string.ConversationUpdateItem_join_call;
} }
} else if (endedRecently) {
text = R.string.ConversationUpdateItem_call_back;
} }
if (text != 0 && conversationRecipient.isGroup() && conversationRecipient.isActiveGroup()) { if (text != 0 && conversationRecipient.isGroup() && conversationRecipient.isActiveGroup()) {

View file

@ -18,11 +18,13 @@ import org.signal.core.util.readToList
import org.signal.core.util.readToMap import org.signal.core.util.readToMap
import org.signal.core.util.readToSingleLong import org.signal.core.util.readToSingleLong
import org.signal.core.util.readToSingleObject import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireLong import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireObject import org.signal.core.util.requireObject
import org.signal.core.util.requireString import org.signal.core.util.requireString
import org.signal.core.util.select import org.signal.core.util.select
import org.signal.core.util.toInt
import org.signal.core.util.toSingleLine import org.signal.core.util.toSingleLine
import org.signal.core.util.update import org.signal.core.util.update
import org.signal.core.util.withinTransaction import org.signal.core.util.withinTransaction
@ -64,6 +66,22 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
const val DELETION_TIMESTAMP = "deletion_timestamp" const val DELETION_TIMESTAMP = "deletion_timestamp"
const val READ = "read" const val READ = "read"
/**
* Whether a given call event was joined by the local user
*
* Used to determine if a group call in the "GENERIC_GROUP_CALL" state is to be
* displayed as a missed call in the ui
*/
const val LOCAL_JOINED = "local_joined"
/**
* Whether a given call event is currently considered active.
*
* Used to determine if a group call in the "GENERIC_GROUP_CALL" state is to be
* displayed as a missed call in the ui
*/
const val GROUP_CALL_ACTIVE = "group_call_active"
//language=sql //language=sql
const val CREATE_TABLE = """ const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME ( CREATE TABLE $TABLE_NAME (
@ -78,6 +96,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
$RINGER INTEGER DEFAULT NULL, $RINGER INTEGER DEFAULT NULL,
$DELETION_TIMESTAMP INTEGER DEFAULT 0, $DELETION_TIMESTAMP INTEGER DEFAULT 0,
$READ INTEGER DEFAULT 1, $READ INTEGER DEFAULT 1,
$LOCAL_JOINED INTEGER DEFAULT 0,
$GROUP_CALL_ACTIVE INTEGER DEFAULT 0,
UNIQUE ($CALL_ID, $PEER) ON CONFLICT FAIL UNIQUE ($CALL_ID, $PEER) ON CONFLICT FAIL
) )
""" """
@ -468,6 +488,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
timestamp, timestamp,
"", "",
emptyList(), emptyList(),
false,
false false
) )
} else { } else {
@ -484,7 +505,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
TYPE to Type.serialize(type), TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(direction), DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp, TIMESTAMP to timestamp,
RINGER to ringer RINGER to ringer,
LOCAL_JOINED to true
) )
.run(SQLiteDatabase.CONFLICT_ABORT) .run(SQLiteDatabase.CONFLICT_ABORT)
} }
@ -508,6 +530,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
timestamp, timestamp,
"", "",
emptyList(), emptyList(),
false,
false false
) )
} else { } else {
@ -524,7 +547,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
TYPE to Type.serialize(type), TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(Direction.INCOMING), DIRECTION to Direction.serialize(Direction.INCOMING),
TIMESTAMP to timestamp, TIMESTAMP to timestamp,
RINGER to null RINGER to null,
LOCAL_JOINED to false
) )
.run(SQLiteDatabase.CONFLICT_ABORT) .run(SQLiteDatabase.CONFLICT_ABORT)
} }
@ -601,7 +625,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
call.messageId, call.messageId,
peekGroupCallEraId, peekGroupCallEraId,
peekJoinedUuids, peekJoinedUuids,
isCallFull isCallFull,
call.event == Event.RINGING
) )
} else { } else {
SignalDatabase.messages.insertGroupCall( SignalDatabase.messages.insertGroupCall(
@ -610,16 +635,19 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
timestamp, timestamp,
peekGroupCallEraId, peekGroupCallEraId,
peekJoinedUuids, peekJoinedUuids,
isCallFull isCallFull,
false
) )
} }
insertCallEventFromGroupUpdate( insertCallEventFromGroupUpdate(
callId, callId = callId,
messageId, messageId = messageId,
sender, sender = sender,
groupRecipient.id, groupRecipientId = groupRecipient.id,
timestamp timestamp = timestamp,
didLocalUserJoin = peekJoinedUuids.contains(Recipient.self().requireServiceId().rawUuid),
isGroupCallActive = peekJoinedUuids.isNotEmpty()
) )
} }
} }
@ -660,7 +688,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
messageId: MessageId?, messageId: MessageId?,
sender: RecipientId, sender: RecipientId,
groupRecipientId: RecipientId, groupRecipientId: RecipientId,
timestamp: Long timestamp: Long,
didLocalUserJoin: Boolean,
isGroupCallActive: Boolean
) { ) {
if (messageId != null) { if (messageId != null) {
val call = getCallById(callId, groupRecipientId) val call = getCallById(callId, groupRecipientId)
@ -677,7 +707,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
TYPE to Type.serialize(Type.GROUP_CALL), TYPE to Type.serialize(Type.GROUP_CALL),
DIRECTION to Direction.serialize(direction), DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp, TIMESTAMP to timestamp,
RINGER to null RINGER to null,
LOCAL_JOINED to didLocalUserJoin,
GROUP_CALL_ACTIVE to isGroupCallActive
) )
.run(SQLiteDatabase.CONFLICT_ABORT) .run(SQLiteDatabase.CONFLICT_ABORT)
@ -692,6 +724,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
setMessageId(callId, messageId) setMessageId(callId, messageId)
Log.d(TAG, "Updated call event message id for newly inserted group call state: $callId") Log.d(TAG, "Updated call event message id for newly inserted group call state: $callId")
} }
updateGroupCallState(call, didLocalUserJoin, isGroupCallActive)
} }
} else { } else {
Log.d(TAG, "Skipping call event processing for null era id.") Log.d(TAG, "Skipping call event processing for null era id.")
@ -701,7 +735,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
} }
/** /**
* Since this does not alter the call table, we can simply pass this directly through to the old handler. * Update necessary call info from peek
*/ */
fun updateGroupCallFromPeek( fun updateGroupCallFromPeek(
threadId: Long, threadId: Long,
@ -709,7 +743,26 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
peekJoinedUuids: Collection<UUID>, peekJoinedUuids: Collection<UUID>,
isCallFull: Boolean isCallFull: Boolean
): Boolean { ): Boolean {
val sameEraId = SignalDatabase.messages.updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull) val callId = peekGroupCallEraId?.let { CallId.fromEra(it) }
val recipientId = SignalDatabase.threads.getRecipientIdForThreadId(threadId)
val call = if (callId != null && recipientId != null) {
getCallById(callId.longValue(), recipientId)
} else {
null
}
val sameEraId = SignalDatabase.messages.updatePreviousGroupCall(
threadId = threadId,
peekGroupCallEraId = peekGroupCallEraId,
peekJoinedUuids = peekJoinedUuids,
isCallFull = isCallFull,
isRingingOnLocalDevice = call?.event == Event.RINGING
)
if (call != null) {
updateGroupCallState(call, peekJoinedUuids)
}
ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers() ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
return sameEraId return sameEraId
} }
@ -742,6 +795,35 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
return call.event != Event.RINGING && call.event != Event.GENERIC_GROUP_CALL return call.event != Event.RINGING && call.event != Event.GENERIC_GROUP_CALL
} }
private fun updateGroupCallState(
call: Call,
peekJoinedUuids: Collection<UUID>
) {
updateGroupCallState(
call,
peekJoinedUuids.contains(Recipient.self().requireServiceId().rawUuid),
peekJoinedUuids.isNotEmpty()
)
}
private fun updateGroupCallState(
call: Call,
hasLocalUserJoined: Boolean,
isGroupCallActive: Boolean
) {
writableDatabase.update(TABLE_NAME)
.values(
LOCAL_JOINED to (call.didLocalUserJoin || hasLocalUserJoined),
GROUP_CALL_ACTIVE to isGroupCallActive
)
.where(
"$CALL_ID = ? AND $PEER = ?",
call.callId,
call.peer.toLong()
)
.run()
}
private fun handleGroupRingState( private fun handleGroupRingState(
ringId: Long, ringId: Long,
groupRecipientId: RecipientId, groupRecipientId: RecipientId,
@ -893,7 +975,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
timestamp = timestamp, timestamp = timestamp,
eraId = "", eraId = "",
joinedUuids = emptyList(), joinedUuids = emptyList(),
isCallFull = false isCallFull = false,
isIncomingGroupCallRingingOnLocalDevice = event == Event.RINGING
) )
db db
@ -1074,9 +1157,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
// endregion // endregion
private fun getCallsCursor(isCount: Boolean, offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): Cursor { private fun getCallsCursor(isCount: Boolean, offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): Cursor {
val isMissedGenericGroupCall = "$EVENT = ${Event.serialize(Event.GENERIC_GROUP_CALL)} AND $LOCAL_JOINED = ${false.toInt()} AND $GROUP_CALL_ACTIVE = ${false.toInt()}"
val filterClause: SqlUtil.Query = when (filter) { val filterClause: SqlUtil.Query = when (filter) {
CallLogFilter.ALL -> SqlUtil.buildQuery("$DELETION_TIMESTAMP = 0") CallLogFilter.ALL -> SqlUtil.buildQuery("$DELETION_TIMESTAMP = 0")
CallLogFilter.MISSED -> SqlUtil.buildQuery("($EVENT = ${Event.serialize(Event.MISSED)} OR $EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)}) AND $DELETION_TIMESTAMP = 0") CallLogFilter.MISSED -> SqlUtil.buildQuery("($EVENT = ${Event.serialize(Event.MISSED)} OR $EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} OR $EVENT = ${Event.serialize(Event.NOT_ACCEPTED)} OR $EVENT = ${Event.serialize(Event.DECLINED)} OR ($isMissedGenericGroupCall)) AND $DELETION_TIMESTAMP = 0")
CallLogFilter.AD_HOC -> SqlUtil.buildQuery("$TYPE = ${Type.serialize(Type.AD_HOC_CALL)} AND $DELETION_TIMESTAMP = 0") CallLogFilter.AD_HOC -> SqlUtil.buildQuery("$TYPE = ${Type.serialize(Type.AD_HOC_CALL)} AND $DELETION_TIMESTAMP = 0")
} }
@ -1112,16 +1196,28 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
val projection = if (isCount) { val projection = if (isCount) {
"COUNT(*)," "COUNT(*),"
} else { } else {
"p.$ID, p.$TIMESTAMP, $EVENT, $DIRECTION, $PEER, p.$TYPE, $CALL_ID, $MESSAGE_ID, $RINGER, children, in_period, ${MessageTable.BODY}," "p.$ID, p.$TIMESTAMP, $EVENT, $DIRECTION, $PEER, p.$TYPE, $CALL_ID, $MESSAGE_ID, $RINGER, $LOCAL_JOINED, $GROUP_CALL_ACTIVE, children, in_period, ${MessageTable.BODY},"
} }
// Group call events by those we consider missed or not missed to build out our call log aggregation.
val eventTypeSubQuery = """ val eventTypeSubQuery = """
($TABLE_NAME.$EVENT = c.$EVENT AND ($TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED)} OR $TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)})) OR ($TABLE_NAME.$EVENT = c.$EVENT AND (
( $TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED)} OR
$TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} OR
$TABLE_NAME.$EVENT = ${Event.serialize(Event.NOT_ACCEPTED)} OR
$TABLE_NAME.$EVENT = ${Event.serialize(Event.DECLINED)} OR
($TABLE_NAME.$isMissedGenericGroupCall)
)) OR (
$TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED)} AND $TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED)} AND
c.$EVENT != ${Event.serialize(Event.MISSED)} AND c.$EVENT != ${Event.serialize(Event.MISSED)} AND
$TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} AND $TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} AND
c.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} c.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} AND
$TABLE_NAME.$EVENT != ${Event.serialize(Event.NOT_ACCEPTED)} AND
c.$EVENT != ${Event.serialize(Event.NOT_ACCEPTED)} AND
$TABLE_NAME.$EVENT != ${Event.serialize(Event.DECLINED)} AND
c.$EVENT != ${Event.serialize(Event.DECLINED)} AND
(NOT ($TABLE_NAME.$isMissedGenericGroupCall)) AND
(NOT (c.$isMissedGenericGroupCall))
) )
""" """
@ -1131,6 +1227,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
LOWER( LOWER(
COALESCE( COALESCE(
NULLIF(${GroupTable.TABLE_NAME}.${GroupTable.TITLE}, ''), NULLIF(${GroupTable.TABLE_NAME}.${GroupTable.TITLE}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.NICKNAME_JOINED_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.NICKNAME_GIVEN_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_JOINED_NAME}, ''), NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_JOINED_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_GIVEN_NAME}, ''), NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_GIVEN_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_JOINED_NAME}, ''), NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_JOINED_NAME}, ''),
@ -1141,7 +1239,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
FROM ( FROM (
WITH cte AS ( WITH cte AS (
SELECT SELECT
$ID, $TIMESTAMP, $EVENT, $DIRECTION, $PEER, $TYPE, $CALL_ID, $MESSAGE_ID, $RINGER, $ID, $TIMESTAMP, $EVENT, $DIRECTION, $PEER, $TYPE, $CALL_ID, $MESSAGE_ID, $RINGER, $LOCAL_JOINED, $GROUP_CALL_ACTIVE,
( (
SELECT SELECT
$ID $ID
@ -1225,10 +1323,20 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
} }
fun markRingingCallsAsMissed() { fun markRingingCallsAsMissed() {
writableDatabase.update(TABLE_NAME) writableDatabase.withinTransaction { db ->
val messageIds: List<Long> = db.select(MESSAGE_ID)
.from(TABLE_NAME)
.where("$EVENT = ? AND $MESSAGE_ID != NULL", Event.serialize(Event.RINGING))
.run()
.readToList { it.requireLong(MESSAGE_ID) }
db.update(TABLE_NAME)
.values(EVENT to Event.serialize(Event.MISSED)) .values(EVENT to Event.serialize(Event.MISSED))
.where("$EVENT = ?", Event.serialize(Event.RINGING)) .where("$EVENT = ?", Event.serialize(Event.RINGING))
.run() .run()
SignalDatabase.messages.clearIsRingingOnLocalDeviceFlag(messageIds)
}
} }
fun getCallsCount(searchTerm: String?, filter: CallLogFilter): Int { fun getCallsCount(searchTerm: String?, filter: CallLogFilter): Int {
@ -1288,6 +1396,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
.run() .run()
} }
/**
* @param isGroupCallActive - Whether the group call currently contains users. Only valid for group calls.
* @param didLocalUserJoin - Determines whether the local user joined this call. Only valid for group calls.
*/
data class Call( data class Call(
val callId: Long, val callId: Long,
val peer: RecipientId, val peer: RecipientId,
@ -1296,11 +1408,20 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
val event: Event, val event: Event,
val messageId: Long?, val messageId: Long?,
val timestamp: Long, val timestamp: Long,
val ringerRecipient: RecipientId? val ringerRecipient: RecipientId?,
val isGroupCallActive: Boolean,
val didLocalUserJoin: Boolean
) { ) {
val messageType: Long = getMessageType(type, direction, event) val messageType: Long = getMessageType(type, direction, event)
val isDisplayedAsMissedCallInUi = isDisplayedAsMissedCallInUi(this)
companion object Deserializer : Serializer<Call, Cursor> { companion object Deserializer : Serializer<Call, Cursor> {
private fun isDisplayedAsMissedCallInUi(call: Call): Boolean {
return call.event in Event.DISPLAY_AS_MISSED_CALL || (call.event == Event.GENERIC_GROUP_CALL && !call.didLocalUserJoin && !call.isGroupCallActive)
}
fun getMessageType(type: Type, direction: Direction, event: Event): Long { fun getMessageType(type: Type, direction: Direction, event: Event): Long {
if (type == Type.GROUP_CALL || type == Type.AD_HOC_CALL) { if (type == Type.GROUP_CALL || type == Type.AD_HOC_CALL) {
return MessageTypes.GROUP_CALL_TYPE return MessageTypes.GROUP_CALL_TYPE
@ -1334,7 +1455,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
} else { } else {
null null
} }
} },
isGroupCallActive = data.requireBoolean(GROUP_CALL_ACTIVE),
didLocalUserJoin = data.requireBoolean(LOCAL_JOINED)
) )
} }
} }
@ -1482,6 +1605,14 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
} }
companion object Serializer : IntSerializer<Event> { companion object Serializer : IntSerializer<Event> {
val DISPLAY_AS_MISSED_CALL = listOf(
MISSED,
MISSED_NOTIFICATION_PROFILE,
DECLINED,
NOT_ACCEPTED
)
override fun serialize(data: Event): Int = data.code override fun serialize(data: Event): Int = data.code
override fun deserialize(data: Int): Event { override fun deserialize(data: Int): Event {

View file

@ -828,7 +828,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
timestamp: Long, timestamp: Long,
eraId: String, eraId: String,
joinedUuids: Collection<UUID>, joinedUuids: Collection<UUID>,
isCallFull: Boolean isCallFull: Boolean,
isIncomingGroupCallRingingOnLocalDevice: Boolean
): MessageId { ): MessageId {
val recipient = Recipient.resolved(groupRecipientId) val recipient = Recipient.resolved(groupRecipientId)
val threadId = threads.getOrCreateThreadIdFor(recipient) val threadId = threads.getOrCreateThreadIdFor(recipient)
@ -840,7 +841,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
startedCallUuid = Recipient.resolved(sender).requireServiceId().toString(), startedCallUuid = Recipient.resolved(sender).requireServiceId().toString(),
startedCallTimestamp = timestamp, startedCallTimestamp = timestamp,
inCallUuids = joinedUuids.map { it.toString() }, inCallUuids = joinedUuids.map { it.toString() },
isCallFull = isCallFull isCallFull = isCallFull,
localUserJoined = joinedUuids.contains(Recipient.self().requireServiceId().rawUuid),
isRingingOnLocalDevice = isIncomingGroupCallRingingOnLocalDevice
).encode() ).encode()
val values = contentValuesOf( val values = contentValuesOf(
@ -893,11 +896,46 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
} }
} }
/**
* Clears the flag in GroupCallUpdateDetailsUtil that specifies that the call is ringing on the local device.
* Called when cleaning up the call ringing state (which can get out of sync in the case of an application crash)
*/
fun clearIsRingingOnLocalDeviceFlag(messageIds: Collection<Long>) {
writableDatabase.withinTransaction { db ->
val queries = SqlUtil.buildCollectionQuery(ID, messageIds)
for (query in queries) {
val messageIdBodyPairs = db.select(ID, BODY)
.from(TABLE_NAME)
.where(query.where, query.whereArgs)
.run()
.readToList { cursor ->
cursor.requireLong(ID) to cursor.requireString(BODY)
}
for ((messageId, body) in messageIdBodyPairs) {
val oldBody = GroupCallUpdateDetailsUtil.parse(body)
if (!oldBody.isRingingOnLocalDevice) {
continue
}
val newBody = GroupCallUpdateDetailsUtil.createUpdatedBody(oldBody, oldBody.inCallUuids, oldBody.isCallFull, false)
db.update(TABLE_NAME)
.values(BODY to newBody)
.where(ID_WHERE, messageId)
.run()
}
}
}
}
fun updateGroupCall( fun updateGroupCall(
messageId: Long, messageId: Long,
eraId: String, eraId: String,
joinedUuids: Collection<UUID>, joinedUuids: Collection<UUID>,
isCallFull: Boolean isCallFull: Boolean,
isRingingOnLocalDevice: Boolean
): MessageId { ): MessageId {
writableDatabase.withinTransaction { db -> writableDatabase.withinTransaction { db ->
val message = try { val message = try {
@ -911,7 +949,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId) val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId)
val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList() val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList()
val contentValues = contentValuesOf( val contentValues = contentValuesOf(
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull) BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull, isRingingOnLocalDevice)
) )
if (sameEraId && containsSelf) { if (sameEraId && containsSelf) {
@ -929,7 +967,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
return MessageId(messageId) return MessageId(messageId)
} }
fun updatePreviousGroupCall(threadId: Long, peekGroupCallEraId: String?, peekJoinedUuids: Collection<UUID>, isCallFull: Boolean): Boolean { fun updatePreviousGroupCall(
threadId: Long,
peekGroupCallEraId: String?,
peekJoinedUuids: Collection<UUID>,
isCallFull: Boolean,
isRingingOnLocalDevice: Boolean
): Boolean {
return writableDatabase.withinTransaction { db -> return writableDatabase.withinTransaction { db ->
val cursor = db val cursor = db
.select(*MMS_PROJECTION) .select(*MMS_PROJECTION)
@ -952,7 +996,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
} }
val contentValues = contentValuesOf( val contentValues = contentValuesOf(
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull) BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull, isRingingOnLocalDevice)
) )
if (sameEraId && containsSelf) { if (sameEraId && containsSelf) {

View file

@ -136,24 +136,21 @@ public final class ThreadBodyUtil {
boolean accepted = call.getEvent() == CallTable.Event.ACCEPTED; boolean accepted = call.getEvent() == CallTable.Event.ACCEPTED;
if (call.getDirection() == CallTable.Direction.OUTGOING) { if (call.getDirection() == CallTable.Direction.OUTGOING) {
if (call.getType() == CallTable.Type.AUDIO_CALL) { if (call.getType() == CallTable.Type.AUDIO_CALL) {
return context.getString(accepted ? R.string.MessageRecord_outgoing_voice_call : R.string.MessageRecord_unanswered_voice_call); return context.getString(R.string.MessageRecord_outgoing_voice_call);
} else { } else {
return context.getString(accepted ? R.string.MessageRecord_outgoing_video_call : R.string.MessageRecord_unanswered_video_call); return context.getString(R.string.MessageRecord_outgoing_video_call);
} }
} else { } else {
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL; boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
boolean isMissed = call.getEvent().isMissedCall();
if (accepted) { if (accepted || !call.isDisplayedAsMissedCallInUi()) {
return context.getString(isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call); return context.getString(isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call);
} else if (isMissed) { } else {
if (call.getEvent() == CallTable.Event.MISSED_NOTIFICATION_PROFILE) { if (call.getEvent() == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
return isVideoCall ? context.getString(R.string.MessageRecord_missed_video_call_notification_profile) : context.getString(R.string.MessageRecord_missed_voice_call_notification_profile); return isVideoCall ? context.getString(R.string.MessageRecord_missed_video_call_notification_profile) : context.getString(R.string.MessageRecord_missed_voice_call_notification_profile);
} else { } else {
return isVideoCall ? context.getString(R.string.MessageRecord_missed_video_call) : context.getString(R.string.MessageRecord_missed_voice_call); return isVideoCall ? context.getString(R.string.MessageRecord_missed_video_call) : context.getString(R.string.MessageRecord_missed_voice_call);
} }
} else {
return isVideoCall ? context.getString(R.string.MessageRecord_you_declined_a_video_call) : context.getString(R.string.MessageRecord_you_declined_a_voice_call);
} }
} }
} else { } else {

View file

@ -82,6 +82,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V221_AddReadColumnT
import org.thoughtcrime.securesms.database.helpers.migration.V222_DataHashRefactor import org.thoughtcrime.securesms.database.helpers.migration.V222_DataHashRefactor
import org.thoughtcrime.securesms.database.helpers.migration.V223_AddNicknameAndNoteFieldsToRecipientTable import org.thoughtcrime.securesms.database.helpers.migration.V223_AddNicknameAndNoteFieldsToRecipientTable
import org.thoughtcrime.securesms.database.helpers.migration.V224_AddAttachmentArchiveColumns import org.thoughtcrime.securesms.database.helpers.migration.V224_AddAttachmentArchiveColumns
import org.thoughtcrime.securesms.database.helpers.migration.V225_AddLocalUserJoinedStateAndGroupCallActiveState
/** /**
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
@ -166,10 +167,11 @@ object SignalDatabaseMigrations {
221 to V221_AddReadColumnToCallEventsTable, 221 to V221_AddReadColumnToCallEventsTable,
222 to V222_DataHashRefactor, 222 to V222_DataHashRefactor,
223 to V223_AddNicknameAndNoteFieldsToRecipientTable, 223 to V223_AddNicknameAndNoteFieldsToRecipientTable,
224 to V224_AddAttachmentArchiveColumns 224 to V224_AddAttachmentArchiveColumns,
225 to V225_AddLocalUserJoinedStateAndGroupCallActiveState
) )
const val DATABASE_VERSION = 224 const val DATABASE_VERSION = 225
@JvmStatic @JvmStatic
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {

View file

@ -0,0 +1,30 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import net.zetetic.database.sqlcipher.SQLiteDatabase
/**
* Adds local user joined state and group call active state to the
* call events table for proper representation of missed non-ringing
* group calls.
*
* Pre-migration call events will display as if the user joined the call.
*/
@Suppress("ClassName")
object V225_AddLocalUserJoinedStateAndGroupCallActiveState : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE call ADD COLUMN local_joined INTEGER DEFAULT 0")
db.execSQL("ALTER TABLE call ADD COLUMN group_call_active INTEGER DEFAULT 0")
/**
* Assume for pre-migration calls that we've joined them all. This avoids
* erroneously marking calls as missed.
*/
db.execSQL("UPDATE call SET local_joined = 1")
}
}

View file

@ -3,20 +3,53 @@ package org.thoughtcrime.securesms.database.model;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.signal.core.util.Base64; import org.signal.core.util.Base64;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public final class GroupCallUpdateDetailsUtil { public final class GroupCallUpdateDetailsUtil {
private static final String TAG = Log.tag(GroupCallUpdateDetailsUtil.class); private static final String TAG = Log.tag(GroupCallUpdateDetailsUtil.class);
private static final long CALL_RECENCY_TIMEOUT = TimeUnit.MINUTES.toMillis(5);
private GroupCallUpdateDetailsUtil() { private GroupCallUpdateDetailsUtil() {
} }
/**
* Generates a group chat update message body from backup data
*/
public static @NonNull String createBodyFromBackup(@NonNull GroupCallChatUpdate groupCallChatUpdate) {
ServiceId.ACI startedCall = groupCallChatUpdate.startedCallAci != null ? ServiceId.ACI.parseOrNull(groupCallChatUpdate.startedCallAci) : null;
GroupCallUpdateDetails details = new GroupCallUpdateDetails.Builder()
.startedCallUuid(Objects.toString(startedCall, null))
.startedCallTimestamp(groupCallChatUpdate.startedCallTimestamp)
.endedCallTimestamp(groupCallChatUpdate.endedCallTimestamp)
.isCallFull(false)
.inCallUuids(groupCallChatUpdate.inCallAcis.stream()
.filter(Objects::nonNull)
.map(ServiceId.ACI::parseOrNull)
.filter(Objects::nonNull)
.map(ServiceId.ACI::toString)
.collect(Collectors.toList())
)
.isRingingOnLocalDevice(false)
.localUserJoined(groupCallChatUpdate.localUserJoined != GroupCallChatUpdate.LocalUserJoined.DID_NOT_JOIN)
.build();
return Base64.encodeWithPadding(details.encode());
}
public static @NonNull GroupCallUpdateDetails parse(@Nullable String body) { public static @NonNull GroupCallUpdateDetails parse(@Nullable String body) {
GroupCallUpdateDetails groupCallUpdateDetails = new GroupCallUpdateDetails(); GroupCallUpdateDetails groupCallUpdateDetails = new GroupCallUpdateDetails();
@ -33,10 +66,38 @@ public final class GroupCallUpdateDetailsUtil {
return groupCallUpdateDetails; return groupCallUpdateDetails;
} }
public static @NonNull String createUpdatedBody(@NonNull GroupCallUpdateDetails groupCallUpdateDetails, @NonNull List<String> inCallUuids, boolean isCallFull) { public static boolean checkCallEndedRecently(@NonNull GroupCallUpdateDetails groupCallUpdateDetails) {
if (groupCallUpdateDetails.endedCallTimestamp == 0) {
return false;
}
long now = System.currentTimeMillis();
if (now > groupCallUpdateDetails.endedCallTimestamp) {
return false;
}
return now - groupCallUpdateDetails.endedCallTimestamp < CALL_RECENCY_TIMEOUT;
}
public static @NonNull String createUpdatedBody(@NonNull GroupCallUpdateDetails groupCallUpdateDetails, @NonNull List<String> inCallUuids, boolean isCallFull, boolean isRingingOnLocalDevice)
{
boolean localUserJoined = groupCallUpdateDetails.localUserJoined || inCallUuids.contains(Recipient.self().requireServiceId().getRawUuid().toString());
long endedTimestamp = groupCallUpdateDetails.endedCallTimestamp;
boolean callBecameEmpty = !groupCallUpdateDetails.inCallUuids.isEmpty() && inCallUuids.isEmpty() && !isRingingOnLocalDevice;
boolean ringTerminatedWithNoUsers = groupCallUpdateDetails.isRingingOnLocalDevice && !isRingingOnLocalDevice && inCallUuids.isEmpty();
if (callBecameEmpty || ringTerminatedWithNoUsers) {
endedTimestamp = System.currentTimeMillis();
} else if (!inCallUuids.isEmpty()) {
endedTimestamp = 0;
}
GroupCallUpdateDetails.Builder builder = groupCallUpdateDetails.newBuilder() GroupCallUpdateDetails.Builder builder = groupCallUpdateDetails.newBuilder()
.isCallFull(isCallFull) .isCallFull(isCallFull)
.inCallUuids(inCallUuids); .inCallUuids(inCallUuids)
.localUserJoined(localUserJoined)
.endedCallTimestamp(endedTimestamp)
.isRingingOnLocalDevice(isRingingOnLocalDevice);
return Base64.encodeWithPadding(builder.build().encode()); return Base64.encodeWithPadding(builder.build().encode());
} }

View file

@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit;
/** /**
* Create a group call update message based on time and joined members. * Create a group call update message based on time and joined members.
@ -53,45 +54,61 @@ public class GroupCallUpdateMessageFactory implements UpdateDescription.Spannabl
} }
private @NonNull String createString() { private @NonNull String createString() {
long endedTimestamp = groupCallUpdateDetails.endedCallTimestamp;
boolean isWithinTimeout = GroupCallUpdateDetailsUtil.checkCallEndedRecently(groupCallUpdateDetails);
String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.startedCallTimestamp); String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.startedCallTimestamp);
boolean isOutgoing = Objects.equals(selfAci.toString(), groupCallUpdateDetails.startedCallUuid);
switch (joinedMembers.size()) { switch (joinedMembers.size()) {
case 0: case 0:
return withTime ? context.getString(R.string.MessageRecord_group_call_s, time) if (isWithinTimeout) {
: context.getString(R.string.MessageRecord_group_call); return withTime ? context.getString(R.string.MessageRecord__the_video_call_has_ended_s, time)
: context.getString(R.string.MessageRecord__the_video_call_has_ended);
} else if (endedTimestamp == 0 || groupCallUpdateDetails.localUserJoined) {
if (isOutgoing) {
return withTime ? context.getString(R.string.MessageRecord__outgoing_video_call_s, time)
: context.getString(R.string.MessageRecord__outgoing_video_call);
} else {
return withTime ? context.getString(R.string.MessageRecord__incoming_video_call_s, time)
: context.getString(R.string.MessageRecord__incoming_video_call);
}
} else {
return withTime ? context.getString(R.string.MessageRecord__missed_video_call_s, time)
: context.getString(R.string.MessageRecord__missed_video_call);
}
case 1: case 1:
if (joinedMembers.get(0).toString().equals(groupCallUpdateDetails.startedCallUuid)) { if (joinedMembers.get(0).toString().equals(groupCallUpdateDetails.startedCallUuid)) {
if (Objects.equals(joinedMembers.get(0), selfAci)) { if (Objects.equals(joinedMembers.get(0), selfAci)) {
return withTime ? context.getString(R.string.MessageRecord_you_started_a_group_call_s, time) return withTime ? context.getString(R.string.MessageRecord__you_started_a_video_call_s, time)
: context.getString(R.string.MessageRecord_you_started_a_group_call); : context.getString(R.string.MessageRecord__you_started_a_video_call);
} else { } else {
return withTime ? context.getString(R.string.MessageRecord_s_started_a_group_call_s, describe(joinedMembers.get(0)), time) return withTime ? context.getString(R.string.MessageRecord__s_started_a_video_call_s, describe(joinedMembers.get(0)), time)
: context.getString(R.string.MessageRecord_s_started_a_group_call, describe(joinedMembers.get(0))); : context.getString(R.string.MessageRecord__s_started_a_video_call, describe(joinedMembers.get(0)));
} }
} else if (Objects.equals(joinedMembers.get(0), selfAci)) { } else if (Objects.equals(joinedMembers.get(0), selfAci)) {
return withTime ? context.getString(R.string.MessageRecord_you_are_in_the_group_call_s1, time) return withTime ? context.getString(R.string.MessageRecord_you_are_in_the_call_s1, time)
: context.getString(R.string.MessageRecord_you_are_in_the_group_call); : context.getString(R.string.MessageRecord_you_are_in_the_call);
} else { } else {
return withTime ? context.getString(R.string.MessageRecord_s_is_in_the_group_call_s, describe(joinedMembers.get(0)), time) return withTime ? context.getString(R.string.MessageRecord_s_is_in_the_call_s, describe(joinedMembers.get(0)), time)
: context.getString(R.string.MessageRecord_s_is_in_the_group_call, describe(joinedMembers.get(0))); : context.getString(R.string.MessageRecord_s_is_in_the_call, describe(joinedMembers.get(0)));
} }
case 2: case 2:
return withTime ? context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call_s1, return withTime ? context.getString(R.string.MessageRecord_s_and_s_are_in_the_call_s1,
describe(joinedMembers.get(0)), describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)), describe(joinedMembers.get(1)),
time) time)
: context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call, : context.getString(R.string.MessageRecord_s_and_s_are_in_the_call,
describe(joinedMembers.get(0)), describe(joinedMembers.get(0)),
describe(joinedMembers.get(1))); describe(joinedMembers.get(1)));
default: default:
int others = joinedMembers.size() - 2; int others = joinedMembers.size() - 2;
return withTime ? context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call_s, return withTime ? context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_call_s,
others, others,
describe(joinedMembers.get(0)), describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)), describe(joinedMembers.get(1)),
others, others,
time) time)
: context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call, : context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_call,
others, others,
describe(joinedMembers.get(0)), describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)), describe(joinedMembers.get(1)),

View file

@ -232,21 +232,20 @@ public class MmsMessageRecord extends MessageRecord {
if (call.getDirection() == CallTable.Direction.OUTGOING) { if (call.getDirection() == CallTable.Direction.OUTGOING) {
if (call.getType() == CallTable.Type.AUDIO_CALL) { if (call.getType() == CallTable.Type.AUDIO_CALL) {
int updateString = accepted ? R.string.MessageRecord_outgoing_voice_call : R.string.MessageRecord_unanswered_voice_call; int updateString = R.string.MessageRecord_outgoing_voice_call;
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_audio_call_outgoing_16); return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_audio_call_outgoing_16);
} else { } else {
int updateString = accepted ? R.string.MessageRecord_outgoing_video_call : R.string.MessageRecord_unanswered_video_call; int updateString = R.string.MessageRecord_outgoing_video_call;
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_video_call_outgoing_16); return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_video_call_outgoing_16);
} }
} else { } else {
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL; boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
boolean isMissed = call.getEvent().isMissedCall();
if (accepted) { if (accepted || !call.isDisplayedAsMissedCallInUi()) {
int updateString = isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call; int updateString = isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call;
int icon = isVideoCall ? R.drawable.ic_update_video_call_incoming_16 : R.drawable.ic_update_audio_call_incoming_16; int icon = isVideoCall ? R.drawable.ic_update_video_call_incoming_16 : R.drawable.ic_update_audio_call_incoming_16;
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon); return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
} else if (isMissed) { } else {
int icon = isVideoCall ? R.drawable.ic_update_video_call_missed_16 : R.drawable.ic_update_audio_call_missed_16; int icon = isVideoCall ? R.drawable.ic_update_video_call_missed_16 : R.drawable.ic_update_audio_call_missed_16;
int message; int message;
if (call.getEvent() == CallTable.Event.MISSED_NOTIFICATION_PROFILE) { if (call.getEvent() == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
@ -261,9 +260,6 @@ public class MmsMessageRecord extends MessageRecord {
icon, icon,
ContextCompat.getColor(context, R.color.core_red_shade), ContextCompat.getColor(context, R.color.core_red_shade),
ContextCompat.getColor(context, R.color.core_red)); ContextCompat.getColor(context, R.color.core_red));
} else {
return isVideoCall ? staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_you_declined_a_video_call), callDateString), R.drawable.ic_update_video_call_incoming_16)
: staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_you_declined_a_voice_call), callDateString), R.drawable.ic_update_audio_call_incoming_16);
} }
} }
} }

View file

@ -516,9 +516,17 @@ message IndividualCallChatUpdate {
} }
message GroupCallChatUpdate { message GroupCallChatUpdate {
enum LocalUserJoined {
UNKNOWN = 0;
JOINED = 1;
DID_NOT_JOIN = 2;
}
optional bytes startedCallAci = 1; optional bytes startedCallAci = 1;
uint64 startedCallTimestamp = 2; uint64 startedCallTimestamp = 2;
repeated bytes inCallAcis = 3; repeated bytes inCallAcis = 3;
uint64 endedCallTimestamp = 4; // 0 indicates we do not know
LocalUserJoined localUserJoined = 5;
} }
message SimpleChatUpdate { message SimpleChatUpdate {

View file

@ -121,6 +121,9 @@ message GroupCallUpdateDetails {
int64 startedCallTimestamp = 3; int64 startedCallTimestamp = 3;
repeated string inCallUuids = 4; repeated string inCallUuids = 4;
bool isCallFull = 5; bool isCallFull = 5;
bool localUserJoined = 6;
int64 endedCallTimestamp = 7;
bool isRingingOnLocalDevice = 8;
} }
message ExpiringProfileKeyCredentialColumnData { message ExpiringProfileKeyCredentialColumnData {

View file

@ -1335,17 +1335,13 @@
<string name="MessageRecord_left_group">You have left the group.</string> <string name="MessageRecord_left_group">You have left the group.</string>
<string name="MessageRecord_you_updated_group">You updated the group.</string> <string name="MessageRecord_you_updated_group">You updated the group.</string>
<string name="MessageRecord_the_group_was_updated">The group was updated.</string> <string name="MessageRecord_the_group_was_updated">The group was updated.</string>
<!-- Update message shown when placing an outgoing 1:1 voice/audio call and it\'s answered by the other party --> <!-- Update message shown when placing an outgoing 1:1 voice/audio call -->
<string name="MessageRecord_outgoing_voice_call">Outgoing voice call</string> <string name="MessageRecord_outgoing_voice_call">Outgoing voice call</string>
<!-- Update message shown when placing an outgoing 1:1 video call and it\'s answered by the other party --> <!-- Update message shown when placing an outgoing 1:1 video call -->
<string name="MessageRecord_outgoing_video_call">Outgoing video call</string> <string name="MessageRecord_outgoing_video_call">Outgoing video call</string>
<!-- Update message shown when placing an outgoing 1:1 voice/audio call and it\'s not answered by the other party --> <!-- Update message shown when receiving an incoming 1:1 voice/audio call and it\'s answered or ringing -->
<string name="MessageRecord_unanswered_voice_call">Unanswered voice call</string>
<!-- Update message shown when placing an outgoing 1:1 video call and it\'s not answered by the other party -->
<string name="MessageRecord_unanswered_video_call">Unanswered video call</string>
<!-- Update message shown when receiving an incoming 1:1 voice/audio call and it\'s answered -->
<string name="MessageRecord_incoming_voice_call">Incoming voice call</string> <string name="MessageRecord_incoming_voice_call">Incoming voice call</string>
<!-- Update message shown when receiving an incoming 1:1 video call and answered --> <!-- Update message shown when receiving an incoming 1:1 video call and answered or ringing -->
<string name="MessageRecord_incoming_video_call">Incoming video call</string> <string name="MessageRecord_incoming_video_call">Incoming video call</string>
<!-- Update message shown when receiving an incoming 1:1 voice/audio call and not answered --> <!-- Update message shown when receiving an incoming 1:1 voice/audio call and not answered -->
<string name="MessageRecord_missed_voice_call">Missed voice call</string> <string name="MessageRecord_missed_voice_call">Missed voice call</string>
@ -1355,10 +1351,6 @@
<string name="MessageRecord_missed_voice_call_notification_profile">Missed voice call while notification profile on</string> <string name="MessageRecord_missed_voice_call_notification_profile">Missed voice call while notification profile on</string>
<!-- Update message shown when receiving an incoming video call and declined due to notification profile --> <!-- Update message shown when receiving an incoming video call and declined due to notification profile -->
<string name="MessageRecord_missed_video_call_notification_profile">Missed video call while notification profile on</string> <string name="MessageRecord_missed_video_call_notification_profile">Missed video call while notification profile on</string>
<!-- Update message shown when receiving an incoming 1:1 voice/audio call and explicitly declined -->
<string name="MessageRecord_you_declined_a_voice_call">You declined a voice call</string>
<!-- Update message shown when receiving an incoming 1:1 video call and explicitly declined -->
<string name="MessageRecord_you_declined_a_video_call">You declined a video call</string>
<!-- Call update formatter string to place the update message next to a time stamp. e.g., \'Incoming voice call · 11:11am\' --> <!-- Call update formatter string to place the update message next to a time stamp. e.g., \'Incoming voice call · 11:11am\' -->
<string name="MessageRecord_call_message_with_date">%1$s · %2$s</string> <string name="MessageRecord_call_message_with_date">%1$s · %2$s</string>
<string name="MessageRecord_s_updated_group">%s updated the group.</string> <string name="MessageRecord_s_updated_group">%s updated the group.</string>
@ -1567,28 +1559,53 @@
<string name="MessageRecord_can_accept_payments">%s can now accept Payments</string> <string name="MessageRecord_can_accept_payments">%s can now accept Payments</string>
<!-- Group Calling update messages --> <!-- Group Calling update messages -->
<string name="MessageRecord_s_started_a_group_call_s">%1$s started a group call · %2$s</string> <!-- Chat log text for an ongoing group call that has one participant with a placeholder for the short display name of the user that joined and a placeholder for formatted time -->
<string name="MessageRecord_you_started_a_group_call_s">You started a group call · %1$s</string> <string name="MessageRecord_s_is_in_the_call_s">%1$s is in the call · %2$s</string>
<string name="MessageRecord_s_is_in_the_group_call_s">%1$s is in the group call · %2$s</string> <!-- Chat log text for an ongoing group call only the local user has joined with a placeholder for formatted time -->
<string name="MessageRecord_you_are_in_the_group_call_s1">You are in the group call · %1$s</string> <string name="MessageRecord_you_are_in_the_call_s1">You are in the call · %1$s</string>
<string name="MessageRecord_s_and_s_are_in_the_group_call_s1">%1$s and %2$s are in the group call · %3$s</string> <!-- Chat log text for an ongoing group call with two participants with two placeholders for the short display name of the users that joined and a placeholder for formatted time -->
<string name="MessageRecord_group_call_s">Group call · %1$s</string> <string name="MessageRecord_s_and_s_are_in_the_call_s1">%1$s and %2$s are in the call · %3$s</string>
<!-- Chat log text for an ongoing group call that has one participant -->
<string name="MessageRecord_s_started_a_group_call">%1$s started a group call</string> <string name="MessageRecord_s_is_in_the_call">%1$s is in the call</string>
<string name="MessageRecord_you_started_a_group_call">You started a group call</string> <!-- Chat log text for an ongoing group call only the local user has joined -->
<string name="MessageRecord_s_is_in_the_group_call">%1$s is in the group call</string> <string name="MessageRecord_you_are_in_the_call">You are in the call</string>
<string name="MessageRecord_you_are_in_the_group_call">You are in the group call</string> <!-- Chat log text for an ongoing group call with two participants with two placeholders, each for the short display name of the users that joined -->
<string name="MessageRecord_s_and_s_are_in_the_group_call">%1$s and %2$s are in the group call</string> <string name="MessageRecord_s_and_s_are_in_the_call">%1$s and %2$s are in the call</string>
<string name="MessageRecord_group_call">Group call</string> <!-- Chat log text for a group call that ended within the last 5 minutes -->
<string name="MessageRecord__the_video_call_has_ended">The video call has ended</string>
<!-- Chat log text for a group call that ended within the last 5 minutes with a placeholder for formatted time -->
<string name="MessageRecord__the_video_call_has_ended_s">The video call has ended · %1$s</string>
<!-- Chat log text for a group call that ended more than 5 minutes ago and was missed -->
<string name="MessageRecord__missed_video_call">Missed video call</string>
<!-- Chat log text for a group call that ended more than 5 minutes ago and was missed with a placeholder for formatted time -->
<string name="MessageRecord__missed_video_call_s">Missed video call · %1$s</string>
<!-- Chat log text for a group call that ended that the local user did not start -->
<string name="MessageRecord__incoming_video_call">Incoming video call</string>
<!-- Chat log text for a group call that ended that the local user did not start with a placeholder for formatted time -->
<string name="MessageRecord__incoming_video_call_s">Incoming video call · %1$s</string>
<!-- Chat log text for a group call that ended that the local user started -->
<string name="MessageRecord__outgoing_video_call">Outgoing video call</string>
<!-- Chat log text for a group call that ended that the local user started with a placeholder for formatted time -->
<string name="MessageRecord__outgoing_video_call_s">Outgoing video call · %1$s</string>
<!-- Chat log text for an ongoing group call that the local user started -->
<string name="MessageRecord__you_started_a_video_call">You started a video call</string>
<!-- Chat log text for an ongoing group call that the local user started with a placeholder for formatted time -->
<string name="MessageRecord__you_started_a_video_call_s">You started a video call · %1$s</string>
<!-- Chat log text for an ongoing group call that the local user has not joined with a placeholder for user's short display name -->
<string name="MessageRecord__s_started_a_video_call">%1$s started a video call</string>
<!-- Chat log text for an ongoing group call that the local user has not joined with a placeholder for user's short display name and a placeholder for formatted time -->
<string name="MessageRecord__s_started_a_video_call_s">%1$s started a video call · %2$s</string>
<string name="MessageRecord_you">You</string> <string name="MessageRecord_you">You</string>
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_group_call_s"> <!-- Chat log text for an ongoing group call with more than two participants with placeholders for the first two joined users, a placeholder for the number of users in the call, and a placeholder for formatted time -->
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_call_s">
<item quantity="one">%1$s, %2$s, and %3$d other are in the group call · %4$s</item> <item quantity="one">%1$s, %2$s, and %3$d other are in the group call · %4$s</item>
<item quantity="other">%1$s, %2$s, and %3$d others are in the group call · %4$s</item> <item quantity="other">%1$s, %2$s, and %3$d others are in the group call · %4$s</item>
</plurals> </plurals>
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_group_call"> <!-- Chat log text for an ongoing group call with more than two participants with placeholders for the first two joined users and a placeholder for the number of users in the call -->
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_call">
<item quantity="one">%1$s, %2$s, and %3$d other are in the group call</item> <item quantity="one">%1$s, %2$s, and %3$d other are in the group call</item>
<item quantity="other">%1$s, %2$s, and %3$d others are in the group call</item> <item quantity="other">%1$s, %2$s, and %3$d others are in the group call</item>
</plurals> </plurals>
@ -2712,6 +2729,8 @@
<string name="ConversationUpdateItem_loading">Loading</string> <string name="ConversationUpdateItem_loading">Loading</string>
<string name="ConversationUpdateItem_learn_more">Learn more</string> <string name="ConversationUpdateItem_learn_more">Learn more</string>
<string name="ConversationUpdateItem_join_call">Join call</string> <string name="ConversationUpdateItem_join_call">Join call</string>
<!-- Action button label for starting a new call in the current conversation -->
<string name="ConversationUpdateItem_call_back">Call back</string>
<string name="ConversationUpdateItem_return_to_call">Return to call</string> <string name="ConversationUpdateItem_return_to_call">Return to call</string>
<string name="ConversationUpdateItem_call_is_full">Call is full</string> <string name="ConversationUpdateItem_call_is_full">Call is full</string>
<string name="ConversationUpdateItem_invite_friends">Invite friends</string> <string name="ConversationUpdateItem_invite_friends">Invite friends</string>