Update call strings to align with new designs.
This commit is contained in:
parent
a83abaca1d
commit
1b7784b01f
17 changed files with 535 additions and 124 deletions
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
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()) {
|
||||
|
|
|
@ -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)
|
||||
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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
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)),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -121,6 +121,9 @@ message GroupCallUpdateDetails {
|
|||
int64 startedCallTimestamp = 3;
|
||||
repeated string inCallUuids = 4;
|
||||
bool isCallFull = 5;
|
||||
bool localUserJoined = 6;
|
||||
int64 endedCallTimestamp = 7;
|
||||
bool isRingingOnLocalDevice = 8;
|
||||
}
|
||||
|
||||
message ExpiringProfileKeyCredentialColumnData {
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Reference in a new issue