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.os.Environment
import androidx.test.platform.app.InstrumentationRegistry
import io.mockk.InternalPlatformDsl.toArray
import okio.ByteString.Companion.toByteString
import org.junit.Assert
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.BodyRange
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.ChatItem
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.Frame
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.ProfileChangeChatUpdate
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(
*standardFrames,
Recipient(
id = 3,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
aci = startedAci,
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
@ -698,8 +752,21 @@ class ImportExportTest {
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(),
*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) -> {
builder.sms = false
val call = calls.getCallByMessageId(record.id)
if (call != null) {
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callId = call.callId))
@ -232,12 +233,23 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
.withoutNulls()
.map { obj: UUID? -> ACI.from(obj!!).toByteString() }
.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(
callingMessage = CallChatUpdate(
groupCall = GroupCallChatUpdate(
startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(),
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.NetworkFailure
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.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
@ -460,6 +461,10 @@ class ChatItemImportInserter(
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
this.put(MessageTable.TYPE, typeFlags)

View file

@ -305,7 +305,7 @@ class CallLogAdapter(
val color = ContextCompat.getColor(
context,
if (call.record.event.isMissedCall()) {
if (call.record.isDisplayedAsMissedCallInUi) {
R.color.signal_colorError
} else {
R.color.signal_colorOnSurfaceVariant
@ -371,11 +371,11 @@ class CallLogAdapter(
private fun getCallStateDrawableRes(call: CallTable.Call): Int {
return when (call.messageType) {
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.GROUP_CALL_TYPE -> when {
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.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16
@ -389,23 +389,19 @@ class CallLogAdapter(
@StringRes
private fun getCallStateStringRes(call: CallTable.Call): Int {
return when (call.messageType) {
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.INCOMING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__incoming
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__incoming
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.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
MessageTypes.GROUP_CALL_TYPE -> when {
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.isDisplayedAsMissedCallInUi -> R.string.CallLogAdapter__missed
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.OUTGOING -> R.string.CallLogAdapter__outgoing
else -> throw AssertionError()
}
else -> error("Unexpected type ${call.messageType}")
else -> if (call.isDisplayedAsMissedCallInUi) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__incoming
}
}
}

View file

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.settings.conversation.preferences
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTypes
@ -46,10 +47,10 @@ object CallPreference {
private fun getCallIcon(call: CallTable.Call): Int {
return when (call.messageType) {
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.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.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_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 {
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_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.INCOMING_AUDIO_CALL_TYPE -> R.string.MessageRecord_incoming_voice_call
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.MessageRecord_incoming_video_call
MessageTypes.MISSED_AUDIO_CALL_TYPE -> getMissedCallString(false, call.event)
MessageTypes.MISSED_VIDEO_CALL_TYPE -> getMissedCallString(true, call.event)
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) getMissedCallString(false, call.event) else R.string.MessageRecord_incoming_voice_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_VIDEO_CALL_TYPE -> R.string.MessageRecord_outgoing_video_call
MessageTypes.GROUP_CALL_TYPE -> when {
call.event == CallTable.Event.MISSED -> R.string.CallPreference__missed_group_call
call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallPreference__missed_group_call_notification_profile
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.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.OUTGOING -> R.string.CallPreference__outgoing_group_call
@ -81,6 +81,23 @@ object CallPreference {
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 {
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.MessageRecord;
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.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
@ -447,11 +448,14 @@ public final class ConversationUpdateItem extends FrameLayout
}
});
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<ACI> acis = updateDescription.getMentioned();
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);
Collection<ACI> acis = updateDescription.getMentioned();
int text = 0;
if (Util.hasItems(acis)) {
if (Util.hasItems(acis) || isRingingOnLocalDevice) {
if (acis.contains(SignalStore.account().requireAci())) {
text = R.string.ConversationUpdateItem_return_to_call;
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).isCallFull) {
@ -459,6 +463,8 @@ public final class ConversationUpdateItem extends FrameLayout
} else {
text = R.string.ConversationUpdateItem_join_call;
}
} else if (endedRecently) {
text = R.string.ConversationUpdateItem_call_back;
}
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.readToSingleLong
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireObject
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.toInt
import org.signal.core.util.toSingleLine
import org.signal.core.util.update
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 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
const val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
@ -78,6 +96,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
$RINGER INTEGER DEFAULT NULL,
$DELETION_TIMESTAMP INTEGER DEFAULT 0,
$READ INTEGER DEFAULT 1,
$LOCAL_JOINED INTEGER DEFAULT 0,
$GROUP_CALL_ACTIVE INTEGER DEFAULT 0,
UNIQUE ($CALL_ID, $PEER) ON CONFLICT FAIL
)
"""
@ -468,6 +488,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
timestamp,
"",
emptyList(),
false,
false
)
} else {
@ -484,7 +505,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
RINGER to ringer
RINGER to ringer,
LOCAL_JOINED to true
)
.run(SQLiteDatabase.CONFLICT_ABORT)
}
@ -508,6 +530,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
timestamp,
"",
emptyList(),
false,
false
)
} else {
@ -524,7 +547,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
TYPE to Type.serialize(type),
DIRECTION to Direction.serialize(Direction.INCOMING),
TIMESTAMP to timestamp,
RINGER to null
RINGER to null,
LOCAL_JOINED to false
)
.run(SQLiteDatabase.CONFLICT_ABORT)
}
@ -601,7 +625,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
call.messageId,
peekGroupCallEraId,
peekJoinedUuids,
isCallFull
isCallFull,
call.event == Event.RINGING
)
} else {
SignalDatabase.messages.insertGroupCall(
@ -610,16 +635,19 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
timestamp,
peekGroupCallEraId,
peekJoinedUuids,
isCallFull
isCallFull,
false
)
}
insertCallEventFromGroupUpdate(
callId,
messageId,
sender,
groupRecipient.id,
timestamp
callId = callId,
messageId = messageId,
sender = sender,
groupRecipientId = groupRecipient.id,
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?,
sender: RecipientId,
groupRecipientId: RecipientId,
timestamp: Long
timestamp: Long,
didLocalUserJoin: Boolean,
isGroupCallActive: Boolean
) {
if (messageId != null) {
val call = getCallById(callId, groupRecipientId)
@ -677,7 +707,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
TYPE to Type.serialize(Type.GROUP_CALL),
DIRECTION to Direction.serialize(direction),
TIMESTAMP to timestamp,
RINGER to null
RINGER to null,
LOCAL_JOINED to didLocalUserJoin,
GROUP_CALL_ACTIVE to isGroupCallActive
)
.run(SQLiteDatabase.CONFLICT_ABORT)
@ -692,6 +724,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
setMessageId(callId, messageId)
Log.d(TAG, "Updated call event message id for newly inserted group call state: $callId")
}
updateGroupCallState(call, didLocalUserJoin, isGroupCallActive)
}
} else {
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(
threadId: Long,
@ -709,7 +743,26 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
peekJoinedUuids: Collection<UUID>,
isCallFull: 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()
return sameEraId
}
@ -742,6 +795,35 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
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(
ringId: Long,
groupRecipientId: RecipientId,
@ -893,7 +975,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
timestamp = timestamp,
eraId = "",
joinedUuids = emptyList(),
isCallFull = false
isCallFull = false,
isIncomingGroupCallRingingOnLocalDevice = event == Event.RINGING
)
db
@ -1074,9 +1157,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
// endregion
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) {
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")
}
@ -1112,16 +1196,28 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
val projection = if (isCount) {
"COUNT(*),"
} 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 = """
($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
c.$EVENT != ${Event.serialize(Event.MISSED)} 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(
COALESCE(
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_GIVEN_NAME}, ''),
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_JOINED_NAME}, ''),
@ -1141,7 +1239,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
FROM (
WITH cte AS (
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
$ID
@ -1225,10 +1323,20 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
}
fun markRingingCallsAsMissed() {
writableDatabase.update(TABLE_NAME)
.values(EVENT to Event.serialize(Event.MISSED))
.where("$EVENT = ?", Event.serialize(Event.RINGING))
.run()
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))
.where("$EVENT = ?", Event.serialize(Event.RINGING))
.run()
SignalDatabase.messages.clearIsRingingOnLocalDeviceFlag(messageIds)
}
}
fun getCallsCount(searchTerm: String?, filter: CallLogFilter): Int {
@ -1288,6 +1396,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
.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(
val callId: Long,
val peer: RecipientId,
@ -1296,11 +1408,20 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
val event: Event,
val messageId: Long?,
val timestamp: Long,
val ringerRecipient: RecipientId?
val ringerRecipient: RecipientId?,
val isGroupCallActive: Boolean,
val didLocalUserJoin: Boolean
) {
val messageType: Long = getMessageType(type, direction, event)
val isDisplayedAsMissedCallInUi = isDisplayedAsMissedCallInUi(this)
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 {
if (type == Type.GROUP_CALL || type == Type.AD_HOC_CALL) {
return MessageTypes.GROUP_CALL_TYPE
@ -1334,7 +1455,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
} else {
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> {
val DISPLAY_AS_MISSED_CALL = listOf(
MISSED,
MISSED_NOTIFICATION_PROFILE,
DECLINED,
NOT_ACCEPTED
)
override fun serialize(data: Event): Int = data.code
override fun deserialize(data: Int): Event {

View file

@ -828,7 +828,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
timestamp: Long,
eraId: String,
joinedUuids: Collection<UUID>,
isCallFull: Boolean
isCallFull: Boolean,
isIncomingGroupCallRingingOnLocalDevice: Boolean
): MessageId {
val recipient = Recipient.resolved(groupRecipientId)
val threadId = threads.getOrCreateThreadIdFor(recipient)
@ -840,7 +841,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
startedCallUuid = Recipient.resolved(sender).requireServiceId().toString(),
startedCallTimestamp = timestamp,
inCallUuids = joinedUuids.map { it.toString() },
isCallFull = isCallFull
isCallFull = isCallFull,
localUserJoined = joinedUuids.contains(Recipient.self().requireServiceId().rawUuid),
isRingingOnLocalDevice = isIncomingGroupCallRingingOnLocalDevice
).encode()
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(
messageId: Long,
eraId: String,
joinedUuids: Collection<UUID>,
isCallFull: Boolean
isCallFull: Boolean,
isRingingOnLocalDevice: Boolean
): MessageId {
writableDatabase.withinTransaction { db ->
val message = try {
@ -911,7 +949,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId)
val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList()
val contentValues = contentValuesOf(
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull)
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull, isRingingOnLocalDevice)
)
if (sameEraId && containsSelf) {
@ -929,7 +967,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
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 ->
val cursor = db
.select(*MMS_PROJECTION)
@ -952,7 +996,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
val contentValues = contentValuesOf(
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull)
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull, isRingingOnLocalDevice)
)
if (sameEraId && containsSelf) {

View file

@ -136,24 +136,21 @@ public final class ThreadBodyUtil {
boolean accepted = call.getEvent() == CallTable.Event.ACCEPTED;
if (call.getDirection() == CallTable.Direction.OUTGOING) {
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 {
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 {
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);
} else if (isMissed) {
} else {
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);
} else {
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 {

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.V223_AddNicknameAndNoteFieldsToRecipientTable
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.
@ -166,10 +167,11 @@ object SignalDatabaseMigrations {
221 to V221_AddReadColumnToCallEventsTable,
222 to V222_DataHashRefactor,
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
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.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.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.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public final class GroupCallUpdateDetailsUtil {
private static final String TAG = Log.tag(GroupCallUpdateDetailsUtil.class);
private static final long CALL_RECENCY_TIMEOUT = TimeUnit.MINUTES.toMillis(5);
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) {
GroupCallUpdateDetails groupCallUpdateDetails = new GroupCallUpdateDetails();
@ -33,10 +66,38 @@ public final class GroupCallUpdateDetailsUtil {
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()
.isCallFull(isCallFull)
.inCallUuids(inCallUuids);
.inCallUuids(inCallUuids)
.localUserJoined(localUserJoined)
.endedCallTimestamp(endedTimestamp)
.isRingingOnLocalDevice(isRingingOnLocalDevice);
return Base64.encodeWithPadding(builder.build().encode());
}

View file

@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 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() {
String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.startedCallTimestamp);
long endedTimestamp = groupCallUpdateDetails.endedCallTimestamp;
boolean isWithinTimeout = GroupCallUpdateDetailsUtil.checkCallEndedRecently(groupCallUpdateDetails);
String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.startedCallTimestamp);
boolean isOutgoing = Objects.equals(selfAci.toString(), groupCallUpdateDetails.startedCallUuid);
switch (joinedMembers.size()) {
case 0:
return withTime ? context.getString(R.string.MessageRecord_group_call_s, time)
: context.getString(R.string.MessageRecord_group_call);
if (isWithinTimeout) {
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:
if (joinedMembers.get(0).toString().equals(groupCallUpdateDetails.startedCallUuid)) {
if (Objects.equals(joinedMembers.get(0), selfAci)) {
return withTime ? context.getString(R.string.MessageRecord_you_started_a_group_call_s, time)
: context.getString(R.string.MessageRecord_you_started_a_group_call);
return withTime ? context.getString(R.string.MessageRecord__you_started_a_video_call_s, time)
: context.getString(R.string.MessageRecord__you_started_a_video_call);
} else {
return withTime ? context.getString(R.string.MessageRecord_s_started_a_group_call_s, describe(joinedMembers.get(0)), time)
: context.getString(R.string.MessageRecord_s_started_a_group_call, describe(joinedMembers.get(0)));
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_video_call, describe(joinedMembers.get(0)));
}
} else if (Objects.equals(joinedMembers.get(0), selfAci)) {
return withTime ? context.getString(R.string.MessageRecord_you_are_in_the_group_call_s1, time)
: context.getString(R.string.MessageRecord_you_are_in_the_group_call);
return withTime ? context.getString(R.string.MessageRecord_you_are_in_the_call_s1, time)
: context.getString(R.string.MessageRecord_you_are_in_the_call);
} else {
return withTime ? context.getString(R.string.MessageRecord_s_is_in_the_group_call_s, describe(joinedMembers.get(0)), time)
: context.getString(R.string.MessageRecord_s_is_in_the_group_call, describe(joinedMembers.get(0)));
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_call, describe(joinedMembers.get(0)));
}
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(1)),
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(1)));
default:
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,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)),
others,
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,
describe(joinedMembers.get(0)),
describe(joinedMembers.get(1)),

View file

@ -232,21 +232,20 @@ public class MmsMessageRecord extends MessageRecord {
if (call.getDirection() == CallTable.Direction.OUTGOING) {
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);
} 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);
}
} else {
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 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);
} else if (isMissed) {
} else {
int icon = isVideoCall ? R.drawable.ic_update_video_call_missed_16 : R.drawable.ic_update_audio_call_missed_16;
int message;
if (call.getEvent() == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
@ -261,9 +260,6 @@ public class MmsMessageRecord extends MessageRecord {
icon,
ContextCompat.getColor(context, R.color.core_red_shade),
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 {
enum LocalUserJoined {
UNKNOWN = 0;
JOINED = 1;
DID_NOT_JOIN = 2;
}
optional bytes startedCallAci = 1;
uint64 startedCallTimestamp = 2;
repeated bytes inCallAcis = 3;
uint64 endedCallTimestamp = 4; // 0 indicates we do not know
LocalUserJoined localUserJoined = 5;
}
message SimpleChatUpdate {

View file

@ -116,11 +116,14 @@ message CryptoValue {
}
message GroupCallUpdateDetails {
string eraId = 1;
string startedCallUuid = 2;
int64 startedCallTimestamp = 3;
repeated string inCallUuids = 4;
bool isCallFull = 5;
string eraId = 1;
string startedCallUuid = 2;
int64 startedCallTimestamp = 3;
repeated string inCallUuids = 4;
bool isCallFull = 5;
bool localUserJoined = 6;
int64 endedCallTimestamp = 7;
bool isRingingOnLocalDevice = 8;
}
message ExpiringProfileKeyCredentialColumnData {

View file

@ -1335,17 +1335,13 @@
<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_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>
<!-- 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>
<!-- Update message shown when placing an outgoing 1:1 voice/audio call and it\'s not answered by the other party -->
<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 -->
<!-- Update message shown when receiving an incoming 1:1 voice/audio call and it\'s answered or ringing -->
<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>
<!-- 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>
@ -1355,10 +1351,6 @@
<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 -->
<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\' -->
<string name="MessageRecord_call_message_with_date">%1$s · %2$s</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>
<!-- Group Calling update messages -->
<string name="MessageRecord_s_started_a_group_call_s">%1$s started a group call · %2$s</string>
<string name="MessageRecord_you_started_a_group_call_s">You started a group call · %1$s</string>
<string name="MessageRecord_s_is_in_the_group_call_s">%1$s is in the group call · %2$s</string>
<string name="MessageRecord_you_are_in_the_group_call_s1">You are in the group 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>
<string name="MessageRecord_group_call_s">Group call · %1$s</string>
<string name="MessageRecord_s_started_a_group_call">%1$s started a group call</string>
<string name="MessageRecord_you_started_a_group_call">You started a group call</string>
<string name="MessageRecord_s_is_in_the_group_call">%1$s is in the group call</string>
<string name="MessageRecord_you_are_in_the_group_call">You are in the group call</string>
<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_group_call">Group call</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_s_is_in_the_call_s">%1$s is in the 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_call_s1">You are in the call · %1$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_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_is_in_the_call">%1$s is in the call</string>
<!-- Chat log text for an ongoing group call only the local user has joined -->
<string name="MessageRecord_you_are_in_the_call">You are in the 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_call">%1$s and %2$s are in the 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>
<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="other">%1$s, %2$s, and %3$d others are in the group call · %4$s</item>
</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="other">%1$s, %2$s, and %3$d others are in the group call</item>
</plurals>
@ -2712,6 +2729,8 @@
<string name="ConversationUpdateItem_loading">Loading</string>
<string name="ConversationUpdateItem_learn_more">Learn more</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_call_is_full">Call is full</string>
<string name="ConversationUpdateItem_invite_friends">Invite friends</string>