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.app.UiAutomation
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import io.mockk.InternalPlatformDsl.toArray
|
||||||
import okio.ByteString.Companion.toByteString
|
import okio.ByteString.Companion.toByteString
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.AccountData
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
|
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Call
|
import org.thoughtcrime.securesms.backup.v2.proto.Call
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
||||||
|
@ -32,6 +34,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
|
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Group
|
import org.thoughtcrime.securesms.backup.v2.proto.Group
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
|
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
|
||||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||||
|
@ -668,12 +672,62 @@ class ImportExportTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sentTime = 0L
|
||||||
|
val individualCallChatItems = individualCalls.map { call ->
|
||||||
|
ChatItem(
|
||||||
|
chatId = 1,
|
||||||
|
authorId = selfRecipient.id,
|
||||||
|
dateSent = sentTime++,
|
||||||
|
sms = false,
|
||||||
|
incoming = ChatItem.IncomingMessageDetails(
|
||||||
|
dateReceived = sentTime + 1,
|
||||||
|
dateServerSent = sentTime,
|
||||||
|
read = true,
|
||||||
|
sealedSender = true
|
||||||
|
),
|
||||||
|
updateMessage = ChatUpdateMessage(
|
||||||
|
callingMessage = CallChatUpdate(
|
||||||
|
callMessage = IndividualCallChatUpdate(
|
||||||
|
type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
|
val startedAci = TestRecipientUtils.nextAci().toByteString()
|
||||||
|
val groupCallChatItems = groupCalls.map { call ->
|
||||||
|
ChatItem(
|
||||||
|
chatId = 1,
|
||||||
|
authorId = selfRecipient.id,
|
||||||
|
dateSent = sentTime++,
|
||||||
|
sms = false,
|
||||||
|
incoming = ChatItem.IncomingMessageDetails(
|
||||||
|
dateReceived = sentTime + 1,
|
||||||
|
dateServerSent = sentTime,
|
||||||
|
read = true,
|
||||||
|
sealedSender = true
|
||||||
|
),
|
||||||
|
updateMessage = ChatUpdateMessage(
|
||||||
|
callingMessage = CallChatUpdate(
|
||||||
|
groupCall = GroupCallChatUpdate(
|
||||||
|
startedCallAci = startedAci,
|
||||||
|
startedCallTimestamp = 0,
|
||||||
|
endedCallTimestamp = 0,
|
||||||
|
localUserJoined = GroupCallChatUpdate.LocalUserJoined.JOINED,
|
||||||
|
inCallAcis = emptyList()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
importExport(
|
importExport(
|
||||||
*standardFrames,
|
*standardFrames,
|
||||||
Recipient(
|
Recipient(
|
||||||
id = 3,
|
id = 3,
|
||||||
contact = Contact(
|
contact = Contact(
|
||||||
aci = TestRecipientUtils.nextAci().toByteString(),
|
aci = startedAci,
|
||||||
pni = TestRecipientUtils.nextPni().toByteString(),
|
pni = TestRecipientUtils.nextPni().toByteString(),
|
||||||
username = "cool.01",
|
username = "cool.01",
|
||||||
e164 = 141255501234,
|
e164 = 141255501234,
|
||||||
|
@ -698,8 +752,21 @@ class ImportExportTest {
|
||||||
name = "Cool test group"
|
name = "Cool test group"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
Chat(
|
||||||
|
id = 1,
|
||||||
|
recipientId = 3,
|
||||||
|
archived = true,
|
||||||
|
pinnedOrder = 1,
|
||||||
|
expirationTimerMs = 1.days.inWholeMilliseconds,
|
||||||
|
muteUntilMs = System.currentTimeMillis(),
|
||||||
|
markedUnread = true,
|
||||||
|
dontNotifyForMentionsIfMuted = true,
|
||||||
|
wallpaper = null
|
||||||
|
),
|
||||||
*individualCalls.toArray(),
|
*individualCalls.toArray(),
|
||||||
*groupCalls.toArray()
|
*groupCalls.toArray(),
|
||||||
|
*individualCallChatItems,
|
||||||
|
*groupCallChatItems
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -200,6 +200,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MessageTypes.isCallLog(record.type) -> {
|
MessageTypes.isCallLog(record.type) -> {
|
||||||
|
builder.sms = false
|
||||||
val call = calls.getCallByMessageId(record.id)
|
val call = calls.getCallByMessageId(record.id)
|
||||||
if (call != null) {
|
if (call != null) {
|
||||||
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callId = call.callId))
|
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callId = call.callId))
|
||||||
|
@ -232,12 +233,23 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||||
.withoutNulls()
|
.withoutNulls()
|
||||||
.map { obj: UUID? -> ACI.from(obj!!).toByteString() }
|
.map { obj: UUID? -> ACI.from(obj!!).toByteString() }
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
|
val localUserJoined: GroupCallChatUpdate.LocalUserJoined = if (groupCallUpdateDetails.localUserJoined) {
|
||||||
|
GroupCallChatUpdate.LocalUserJoined.JOINED
|
||||||
|
} else if (groupCallUpdateDetails.endedCallTimestamp == 0L) {
|
||||||
|
GroupCallChatUpdate.LocalUserJoined.UNKNOWN
|
||||||
|
} else {
|
||||||
|
GroupCallChatUpdate.LocalUserJoined.DID_NOT_JOIN
|
||||||
|
}
|
||||||
|
|
||||||
builder.updateMessage = ChatUpdateMessage(
|
builder.updateMessage = ChatUpdateMessage(
|
||||||
callingMessage = CallChatUpdate(
|
callingMessage = CallChatUpdate(
|
||||||
groupCall = GroupCallChatUpdate(
|
groupCall = GroupCallChatUpdate(
|
||||||
startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(),
|
startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(),
|
||||||
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
|
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
|
||||||
inCallAcis = joinedMembers
|
inCallAcis = joinedMembers,
|
||||||
|
localUserJoined = localUserJoined,
|
||||||
|
endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch
|
||||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
|
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
|
||||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure
|
import org.thoughtcrime.securesms.database.documents.NetworkFailure
|
||||||
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
|
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
|
||||||
|
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
|
||||||
import org.thoughtcrime.securesms.database.model.Mention
|
import org.thoughtcrime.securesms.database.model.Mention
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||||
|
@ -460,6 +461,10 @@ class ChatItemImportInserter(
|
||||||
IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags
|
IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateMessage.callingMessage.groupCall != null -> {
|
||||||
|
typeFlags = MessageTypes.GROUP_CALL_TYPE
|
||||||
|
this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.callingMessage.groupCall))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Calls don't use the incoming/outgoing flags, so we overwrite the flags here
|
// Calls don't use the incoming/outgoing flags, so we overwrite the flags here
|
||||||
this.put(MessageTable.TYPE, typeFlags)
|
this.put(MessageTable.TYPE, typeFlags)
|
||||||
|
|
|
@ -305,7 +305,7 @@ class CallLogAdapter(
|
||||||
|
|
||||||
val color = ContextCompat.getColor(
|
val color = ContextCompat.getColor(
|
||||||
context,
|
context,
|
||||||
if (call.record.event.isMissedCall()) {
|
if (call.record.isDisplayedAsMissedCallInUi) {
|
||||||
R.color.signal_colorError
|
R.color.signal_colorError
|
||||||
} else {
|
} else {
|
||||||
R.color.signal_colorOnSurfaceVariant
|
R.color.signal_colorOnSurfaceVariant
|
||||||
|
@ -371,11 +371,11 @@ class CallLogAdapter(
|
||||||
private fun getCallStateDrawableRes(call: CallTable.Call): Int {
|
private fun getCallStateDrawableRes(call: CallTable.Call): Int {
|
||||||
return when (call.messageType) {
|
return when (call.messageType) {
|
||||||
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_compact_16
|
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_compact_16
|
||||||
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_compact_16
|
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) R.drawable.symbol_missed_incoming_compact_16 else R.drawable.symbol_arrow_downleft_compact_16
|
||||||
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16
|
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16
|
||||||
MessageTypes.GROUP_CALL_TYPE -> when {
|
MessageTypes.GROUP_CALL_TYPE -> when {
|
||||||
call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16
|
call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16
|
||||||
call.event.isMissedCall() -> R.drawable.symbol_missed_incoming_compact_16
|
call.isDisplayedAsMissedCallInUi -> R.drawable.symbol_missed_incoming_compact_16
|
||||||
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16
|
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16
|
||||||
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16
|
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16
|
||||||
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16
|
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16
|
||||||
|
@ -389,23 +389,19 @@ class CallLogAdapter(
|
||||||
@StringRes
|
@StringRes
|
||||||
private fun getCallStateStringRes(call: CallTable.Call): Int {
|
private fun getCallStateStringRes(call: CallTable.Call): Int {
|
||||||
return when (call.messageType) {
|
return when (call.messageType) {
|
||||||
MessageTypes.MISSED_VIDEO_CALL_TYPE,
|
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__missed_notification_profile
|
||||||
MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__missed_notification_profile
|
|
||||||
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__incoming
|
|
||||||
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__incoming
|
|
||||||
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
|
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
|
||||||
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
|
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
|
||||||
MessageTypes.GROUP_CALL_TYPE -> when {
|
MessageTypes.GROUP_CALL_TYPE -> when {
|
||||||
call.type == CallTable.Type.AD_HOC_CALL -> R.string.CallLogAdapter__call_link
|
call.type == CallTable.Type.AD_HOC_CALL -> R.string.CallLogAdapter__call_link
|
||||||
call.event == CallTable.Event.MISSED -> R.string.CallLogAdapter__missed
|
|
||||||
call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallLogAdapter__missed_notification_profile
|
call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallLogAdapter__missed_notification_profile
|
||||||
|
call.isDisplayedAsMissedCallInUi -> R.string.CallLogAdapter__missed
|
||||||
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
|
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
|
||||||
call.direction == CallTable.Direction.INCOMING -> R.string.CallLogAdapter__incoming
|
call.direction == CallTable.Direction.INCOMING -> R.string.CallLogAdapter__incoming
|
||||||
call.direction == CallTable.Direction.OUTGOING -> R.string.CallLogAdapter__outgoing
|
call.direction == CallTable.Direction.OUTGOING -> R.string.CallLogAdapter__outgoing
|
||||||
else -> throw AssertionError()
|
else -> throw AssertionError()
|
||||||
}
|
}
|
||||||
|
else -> if (call.isDisplayedAsMissedCallInUi) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__incoming
|
||||||
else -> error("Unexpected type ${call.messageType}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.database.CallTable
|
import org.thoughtcrime.securesms.database.CallTable
|
||||||
import org.thoughtcrime.securesms.database.MessageTypes
|
import org.thoughtcrime.securesms.database.MessageTypes
|
||||||
|
@ -46,10 +47,10 @@ object CallPreference {
|
||||||
private fun getCallIcon(call: CallTable.Call): Int {
|
private fun getCallIcon(call: CallTable.Call): Int {
|
||||||
return when (call.messageType) {
|
return when (call.messageType) {
|
||||||
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_24
|
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_24
|
||||||
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_24
|
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) R.drawable.symbol_missed_incoming_24 else R.drawable.symbol_arrow_downleft_24
|
||||||
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_24
|
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_24
|
||||||
MessageTypes.GROUP_CALL_TYPE -> when {
|
MessageTypes.GROUP_CALL_TYPE -> when {
|
||||||
call.event.isMissedCall() -> R.drawable.symbol_missed_incoming_24
|
call.isDisplayedAsMissedCallInUi -> R.drawable.symbol_missed_incoming_24
|
||||||
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_24
|
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_24
|
||||||
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_24
|
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_24
|
||||||
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_24
|
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_24
|
||||||
|
@ -61,15 +62,14 @@ object CallPreference {
|
||||||
|
|
||||||
private fun getCallType(call: CallTable.Call): String {
|
private fun getCallType(call: CallTable.Call): String {
|
||||||
val id = when (call.messageType) {
|
val id = when (call.messageType) {
|
||||||
MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.MessageRecord_missed_voice_call else R.string.MessageRecord_missed_voice_call_notification_profile
|
MessageTypes.MISSED_AUDIO_CALL_TYPE -> getMissedCallString(false, call.event)
|
||||||
MessageTypes.MISSED_VIDEO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.MessageRecord_missed_video_call else R.string.MessageRecord_missed_video_call_notification_profile
|
MessageTypes.MISSED_VIDEO_CALL_TYPE -> getMissedCallString(true, call.event)
|
||||||
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.MessageRecord_incoming_voice_call
|
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) getMissedCallString(false, call.event) else R.string.MessageRecord_incoming_voice_call
|
||||||
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.MessageRecord_incoming_video_call
|
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) getMissedCallString(true, call.event) else R.string.MessageRecord_incoming_video_call
|
||||||
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.MessageRecord_outgoing_voice_call
|
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.MessageRecord_outgoing_voice_call
|
||||||
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.MessageRecord_outgoing_video_call
|
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.MessageRecord_outgoing_video_call
|
||||||
MessageTypes.GROUP_CALL_TYPE -> when {
|
MessageTypes.GROUP_CALL_TYPE -> when {
|
||||||
call.event == CallTable.Event.MISSED -> R.string.CallPreference__missed_group_call
|
call.isDisplayedAsMissedCallInUi -> if (call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE) R.string.CallPreference__missed_group_call_notification_profile else R.string.CallPreference__missed_group_call
|
||||||
call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallPreference__missed_group_call_notification_profile
|
|
||||||
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
|
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
|
||||||
call.direction == CallTable.Direction.INCOMING -> R.string.CallPreference__incoming_group_call
|
call.direction == CallTable.Direction.INCOMING -> R.string.CallPreference__incoming_group_call
|
||||||
call.direction == CallTable.Direction.OUTGOING -> R.string.CallPreference__outgoing_group_call
|
call.direction == CallTable.Direction.OUTGOING -> R.string.CallPreference__outgoing_group_call
|
||||||
|
@ -81,6 +81,23 @@ object CallPreference {
|
||||||
return context.getString(id)
|
return context.getString(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@StringRes
|
||||||
|
private fun getMissedCallString(isVideo: Boolean, callEvent: CallTable.Event): Int {
|
||||||
|
return if (callEvent == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
|
||||||
|
if (isVideo) {
|
||||||
|
R.string.MessageRecord_missed_video_call_notification_profile
|
||||||
|
} else {
|
||||||
|
R.string.MessageRecord_missed_voice_call_notification_profile
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isVideo) {
|
||||||
|
R.string.MessageRecord_missed_video_call
|
||||||
|
} else {
|
||||||
|
R.string.MessageRecord_missed_voice_call
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getCallTime(messageRecord: MessageRecord): String {
|
private fun getCallTime(messageRecord: MessageRecord): String {
|
||||||
return DateUtils.getOnlyTimeString(context, messageRecord.timestamp)
|
return DateUtils.getOnlyTimeString(context, messageRecord.timestamp)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
||||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||||
|
@ -447,11 +448,14 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
|
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
|
||||||
|
GroupCallUpdateDetails groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody());
|
||||||
|
boolean isRingingOnLocalDevice = groupCallUpdateDetails.isRingingOnLocalDevice;
|
||||||
|
boolean endedRecently = GroupCallUpdateDetailsUtil.checkCallEndedRecently(groupCallUpdateDetails);
|
||||||
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
|
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
|
||||||
Collection<ACI> acis = updateDescription.getMentioned();
|
Collection<ACI> acis = updateDescription.getMentioned();
|
||||||
|
|
||||||
int text = 0;
|
int text = 0;
|
||||||
if (Util.hasItems(acis)) {
|
if (Util.hasItems(acis) || isRingingOnLocalDevice) {
|
||||||
if (acis.contains(SignalStore.account().requireAci())) {
|
if (acis.contains(SignalStore.account().requireAci())) {
|
||||||
text = R.string.ConversationUpdateItem_return_to_call;
|
text = R.string.ConversationUpdateItem_return_to_call;
|
||||||
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).isCallFull) {
|
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).isCallFull) {
|
||||||
|
@ -459,6 +463,8 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||||
} else {
|
} else {
|
||||||
text = R.string.ConversationUpdateItem_join_call;
|
text = R.string.ConversationUpdateItem_join_call;
|
||||||
}
|
}
|
||||||
|
} else if (endedRecently) {
|
||||||
|
text = R.string.ConversationUpdateItem_call_back;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (text != 0 && conversationRecipient.isGroup() && conversationRecipient.isActiveGroup()) {
|
if (text != 0 && conversationRecipient.isGroup() && conversationRecipient.isActiveGroup()) {
|
||||||
|
|
|
@ -18,11 +18,13 @@ import org.signal.core.util.readToList
|
||||||
import org.signal.core.util.readToMap
|
import org.signal.core.util.readToMap
|
||||||
import org.signal.core.util.readToSingleLong
|
import org.signal.core.util.readToSingleLong
|
||||||
import org.signal.core.util.readToSingleObject
|
import org.signal.core.util.readToSingleObject
|
||||||
|
import org.signal.core.util.requireBoolean
|
||||||
import org.signal.core.util.requireLong
|
import org.signal.core.util.requireLong
|
||||||
import org.signal.core.util.requireNonNullString
|
import org.signal.core.util.requireNonNullString
|
||||||
import org.signal.core.util.requireObject
|
import org.signal.core.util.requireObject
|
||||||
import org.signal.core.util.requireString
|
import org.signal.core.util.requireString
|
||||||
import org.signal.core.util.select
|
import org.signal.core.util.select
|
||||||
|
import org.signal.core.util.toInt
|
||||||
import org.signal.core.util.toSingleLine
|
import org.signal.core.util.toSingleLine
|
||||||
import org.signal.core.util.update
|
import org.signal.core.util.update
|
||||||
import org.signal.core.util.withinTransaction
|
import org.signal.core.util.withinTransaction
|
||||||
|
@ -64,6 +66,22 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
const val DELETION_TIMESTAMP = "deletion_timestamp"
|
||||||
const val READ = "read"
|
const val READ = "read"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a given call event was joined by the local user
|
||||||
|
*
|
||||||
|
* Used to determine if a group call in the "GENERIC_GROUP_CALL" state is to be
|
||||||
|
* displayed as a missed call in the ui
|
||||||
|
*/
|
||||||
|
const val LOCAL_JOINED = "local_joined"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a given call event is currently considered active.
|
||||||
|
*
|
||||||
|
* Used to determine if a group call in the "GENERIC_GROUP_CALL" state is to be
|
||||||
|
* displayed as a missed call in the ui
|
||||||
|
*/
|
||||||
|
const val GROUP_CALL_ACTIVE = "group_call_active"
|
||||||
|
|
||||||
//language=sql
|
//language=sql
|
||||||
const val CREATE_TABLE = """
|
const val CREATE_TABLE = """
|
||||||
CREATE TABLE $TABLE_NAME (
|
CREATE TABLE $TABLE_NAME (
|
||||||
|
@ -78,6 +96,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
$RINGER INTEGER DEFAULT NULL,
|
$RINGER INTEGER DEFAULT NULL,
|
||||||
$DELETION_TIMESTAMP INTEGER DEFAULT 0,
|
$DELETION_TIMESTAMP INTEGER DEFAULT 0,
|
||||||
$READ INTEGER DEFAULT 1,
|
$READ INTEGER DEFAULT 1,
|
||||||
|
$LOCAL_JOINED INTEGER DEFAULT 0,
|
||||||
|
$GROUP_CALL_ACTIVE INTEGER DEFAULT 0,
|
||||||
UNIQUE ($CALL_ID, $PEER) ON CONFLICT FAIL
|
UNIQUE ($CALL_ID, $PEER) ON CONFLICT FAIL
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
@ -468,6 +488,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
timestamp,
|
timestamp,
|
||||||
"",
|
"",
|
||||||
emptyList(),
|
emptyList(),
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -484,7 +505,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
TYPE to Type.serialize(type),
|
TYPE to Type.serialize(type),
|
||||||
DIRECTION to Direction.serialize(direction),
|
DIRECTION to Direction.serialize(direction),
|
||||||
TIMESTAMP to timestamp,
|
TIMESTAMP to timestamp,
|
||||||
RINGER to ringer
|
RINGER to ringer,
|
||||||
|
LOCAL_JOINED to true
|
||||||
)
|
)
|
||||||
.run(SQLiteDatabase.CONFLICT_ABORT)
|
.run(SQLiteDatabase.CONFLICT_ABORT)
|
||||||
}
|
}
|
||||||
|
@ -508,6 +530,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
timestamp,
|
timestamp,
|
||||||
"",
|
"",
|
||||||
emptyList(),
|
emptyList(),
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -524,7 +547,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
TYPE to Type.serialize(type),
|
TYPE to Type.serialize(type),
|
||||||
DIRECTION to Direction.serialize(Direction.INCOMING),
|
DIRECTION to Direction.serialize(Direction.INCOMING),
|
||||||
TIMESTAMP to timestamp,
|
TIMESTAMP to timestamp,
|
||||||
RINGER to null
|
RINGER to null,
|
||||||
|
LOCAL_JOINED to false
|
||||||
)
|
)
|
||||||
.run(SQLiteDatabase.CONFLICT_ABORT)
|
.run(SQLiteDatabase.CONFLICT_ABORT)
|
||||||
}
|
}
|
||||||
|
@ -601,7 +625,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
call.messageId,
|
call.messageId,
|
||||||
peekGroupCallEraId,
|
peekGroupCallEraId,
|
||||||
peekJoinedUuids,
|
peekJoinedUuids,
|
||||||
isCallFull
|
isCallFull,
|
||||||
|
call.event == Event.RINGING
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
SignalDatabase.messages.insertGroupCall(
|
SignalDatabase.messages.insertGroupCall(
|
||||||
|
@ -610,16 +635,19 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
timestamp,
|
timestamp,
|
||||||
peekGroupCallEraId,
|
peekGroupCallEraId,
|
||||||
peekJoinedUuids,
|
peekJoinedUuids,
|
||||||
isCallFull
|
isCallFull,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
insertCallEventFromGroupUpdate(
|
insertCallEventFromGroupUpdate(
|
||||||
callId,
|
callId = callId,
|
||||||
messageId,
|
messageId = messageId,
|
||||||
sender,
|
sender = sender,
|
||||||
groupRecipient.id,
|
groupRecipientId = groupRecipient.id,
|
||||||
timestamp
|
timestamp = timestamp,
|
||||||
|
didLocalUserJoin = peekJoinedUuids.contains(Recipient.self().requireServiceId().rawUuid),
|
||||||
|
isGroupCallActive = peekJoinedUuids.isNotEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -660,7 +688,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
messageId: MessageId?,
|
messageId: MessageId?,
|
||||||
sender: RecipientId,
|
sender: RecipientId,
|
||||||
groupRecipientId: RecipientId,
|
groupRecipientId: RecipientId,
|
||||||
timestamp: Long
|
timestamp: Long,
|
||||||
|
didLocalUserJoin: Boolean,
|
||||||
|
isGroupCallActive: Boolean
|
||||||
) {
|
) {
|
||||||
if (messageId != null) {
|
if (messageId != null) {
|
||||||
val call = getCallById(callId, groupRecipientId)
|
val call = getCallById(callId, groupRecipientId)
|
||||||
|
@ -677,7 +707,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
TYPE to Type.serialize(Type.GROUP_CALL),
|
TYPE to Type.serialize(Type.GROUP_CALL),
|
||||||
DIRECTION to Direction.serialize(direction),
|
DIRECTION to Direction.serialize(direction),
|
||||||
TIMESTAMP to timestamp,
|
TIMESTAMP to timestamp,
|
||||||
RINGER to null
|
RINGER to null,
|
||||||
|
LOCAL_JOINED to didLocalUserJoin,
|
||||||
|
GROUP_CALL_ACTIVE to isGroupCallActive
|
||||||
)
|
)
|
||||||
.run(SQLiteDatabase.CONFLICT_ABORT)
|
.run(SQLiteDatabase.CONFLICT_ABORT)
|
||||||
|
|
||||||
|
@ -692,6 +724,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
setMessageId(callId, messageId)
|
setMessageId(callId, messageId)
|
||||||
Log.d(TAG, "Updated call event message id for newly inserted group call state: $callId")
|
Log.d(TAG, "Updated call event message id for newly inserted group call state: $callId")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateGroupCallState(call, didLocalUserJoin, isGroupCallActive)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Skipping call event processing for null era id.")
|
Log.d(TAG, "Skipping call event processing for null era id.")
|
||||||
|
@ -701,7 +735,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since this does not alter the call table, we can simply pass this directly through to the old handler.
|
* Update necessary call info from peek
|
||||||
*/
|
*/
|
||||||
fun updateGroupCallFromPeek(
|
fun updateGroupCallFromPeek(
|
||||||
threadId: Long,
|
threadId: Long,
|
||||||
|
@ -709,7 +743,26 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
peekJoinedUuids: Collection<UUID>,
|
peekJoinedUuids: Collection<UUID>,
|
||||||
isCallFull: Boolean
|
isCallFull: Boolean
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val sameEraId = SignalDatabase.messages.updatePreviousGroupCall(threadId, peekGroupCallEraId, peekJoinedUuids, isCallFull)
|
val callId = peekGroupCallEraId?.let { CallId.fromEra(it) }
|
||||||
|
val recipientId = SignalDatabase.threads.getRecipientIdForThreadId(threadId)
|
||||||
|
val call = if (callId != null && recipientId != null) {
|
||||||
|
getCallById(callId.longValue(), recipientId)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val sameEraId = SignalDatabase.messages.updatePreviousGroupCall(
|
||||||
|
threadId = threadId,
|
||||||
|
peekGroupCallEraId = peekGroupCallEraId,
|
||||||
|
peekJoinedUuids = peekJoinedUuids,
|
||||||
|
isCallFull = isCallFull,
|
||||||
|
isRingingOnLocalDevice = call?.event == Event.RINGING
|
||||||
|
)
|
||||||
|
|
||||||
|
if (call != null) {
|
||||||
|
updateGroupCallState(call, peekJoinedUuids)
|
||||||
|
}
|
||||||
|
|
||||||
ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
|
ApplicationDependencies.getDatabaseObserver().notifyCallUpdateObservers()
|
||||||
return sameEraId
|
return sameEraId
|
||||||
}
|
}
|
||||||
|
@ -742,6 +795,35 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
return call.event != Event.RINGING && call.event != Event.GENERIC_GROUP_CALL
|
return call.event != Event.RINGING && call.event != Event.GENERIC_GROUP_CALL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateGroupCallState(
|
||||||
|
call: Call,
|
||||||
|
peekJoinedUuids: Collection<UUID>
|
||||||
|
) {
|
||||||
|
updateGroupCallState(
|
||||||
|
call,
|
||||||
|
peekJoinedUuids.contains(Recipient.self().requireServiceId().rawUuid),
|
||||||
|
peekJoinedUuids.isNotEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateGroupCallState(
|
||||||
|
call: Call,
|
||||||
|
hasLocalUserJoined: Boolean,
|
||||||
|
isGroupCallActive: Boolean
|
||||||
|
) {
|
||||||
|
writableDatabase.update(TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
LOCAL_JOINED to (call.didLocalUserJoin || hasLocalUserJoined),
|
||||||
|
GROUP_CALL_ACTIVE to isGroupCallActive
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
"$CALL_ID = ? AND $PEER = ?",
|
||||||
|
call.callId,
|
||||||
|
call.peer.toLong()
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleGroupRingState(
|
private fun handleGroupRingState(
|
||||||
ringId: Long,
|
ringId: Long,
|
||||||
groupRecipientId: RecipientId,
|
groupRecipientId: RecipientId,
|
||||||
|
@ -893,7 +975,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
eraId = "",
|
eraId = "",
|
||||||
joinedUuids = emptyList(),
|
joinedUuids = emptyList(),
|
||||||
isCallFull = false
|
isCallFull = false,
|
||||||
|
isIncomingGroupCallRingingOnLocalDevice = event == Event.RINGING
|
||||||
)
|
)
|
||||||
|
|
||||||
db
|
db
|
||||||
|
@ -1074,9 +1157,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
private fun getCallsCursor(isCount: Boolean, offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): Cursor {
|
private fun getCallsCursor(isCount: Boolean, offset: Int, limit: Int, searchTerm: String?, filter: CallLogFilter): Cursor {
|
||||||
|
val isMissedGenericGroupCall = "$EVENT = ${Event.serialize(Event.GENERIC_GROUP_CALL)} AND $LOCAL_JOINED = ${false.toInt()} AND $GROUP_CALL_ACTIVE = ${false.toInt()}"
|
||||||
val filterClause: SqlUtil.Query = when (filter) {
|
val filterClause: SqlUtil.Query = when (filter) {
|
||||||
CallLogFilter.ALL -> SqlUtil.buildQuery("$DELETION_TIMESTAMP = 0")
|
CallLogFilter.ALL -> SqlUtil.buildQuery("$DELETION_TIMESTAMP = 0")
|
||||||
CallLogFilter.MISSED -> SqlUtil.buildQuery("($EVENT = ${Event.serialize(Event.MISSED)} OR $EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)}) AND $DELETION_TIMESTAMP = 0")
|
CallLogFilter.MISSED -> SqlUtil.buildQuery("($EVENT = ${Event.serialize(Event.MISSED)} OR $EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} OR $EVENT = ${Event.serialize(Event.NOT_ACCEPTED)} OR $EVENT = ${Event.serialize(Event.DECLINED)} OR ($isMissedGenericGroupCall)) AND $DELETION_TIMESTAMP = 0")
|
||||||
CallLogFilter.AD_HOC -> SqlUtil.buildQuery("$TYPE = ${Type.serialize(Type.AD_HOC_CALL)} AND $DELETION_TIMESTAMP = 0")
|
CallLogFilter.AD_HOC -> SqlUtil.buildQuery("$TYPE = ${Type.serialize(Type.AD_HOC_CALL)} AND $DELETION_TIMESTAMP = 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1112,16 +1196,28 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
val projection = if (isCount) {
|
val projection = if (isCount) {
|
||||||
"COUNT(*),"
|
"COUNT(*),"
|
||||||
} else {
|
} else {
|
||||||
"p.$ID, p.$TIMESTAMP, $EVENT, $DIRECTION, $PEER, p.$TYPE, $CALL_ID, $MESSAGE_ID, $RINGER, children, in_period, ${MessageTable.BODY},"
|
"p.$ID, p.$TIMESTAMP, $EVENT, $DIRECTION, $PEER, p.$TYPE, $CALL_ID, $MESSAGE_ID, $RINGER, $LOCAL_JOINED, $GROUP_CALL_ACTIVE, children, in_period, ${MessageTable.BODY},"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Group call events by those we consider missed or not missed to build out our call log aggregation.
|
||||||
val eventTypeSubQuery = """
|
val eventTypeSubQuery = """
|
||||||
($TABLE_NAME.$EVENT = c.$EVENT AND ($TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED)} OR $TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)})) OR
|
($TABLE_NAME.$EVENT = c.$EVENT AND (
|
||||||
(
|
$TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED)} OR
|
||||||
|
$TABLE_NAME.$EVENT = ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} OR
|
||||||
|
$TABLE_NAME.$EVENT = ${Event.serialize(Event.NOT_ACCEPTED)} OR
|
||||||
|
$TABLE_NAME.$EVENT = ${Event.serialize(Event.DECLINED)} OR
|
||||||
|
($TABLE_NAME.$isMissedGenericGroupCall)
|
||||||
|
)) OR (
|
||||||
$TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED)} AND
|
$TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED)} AND
|
||||||
c.$EVENT != ${Event.serialize(Event.MISSED)} AND
|
c.$EVENT != ${Event.serialize(Event.MISSED)} AND
|
||||||
$TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} AND
|
$TABLE_NAME.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} AND
|
||||||
c.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)}
|
c.$EVENT != ${Event.serialize(Event.MISSED_NOTIFICATION_PROFILE)} AND
|
||||||
|
$TABLE_NAME.$EVENT != ${Event.serialize(Event.NOT_ACCEPTED)} AND
|
||||||
|
c.$EVENT != ${Event.serialize(Event.NOT_ACCEPTED)} AND
|
||||||
|
$TABLE_NAME.$EVENT != ${Event.serialize(Event.DECLINED)} AND
|
||||||
|
c.$EVENT != ${Event.serialize(Event.DECLINED)} AND
|
||||||
|
(NOT ($TABLE_NAME.$isMissedGenericGroupCall)) AND
|
||||||
|
(NOT (c.$isMissedGenericGroupCall))
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1131,6 +1227,8 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
LOWER(
|
LOWER(
|
||||||
COALESCE(
|
COALESCE(
|
||||||
NULLIF(${GroupTable.TABLE_NAME}.${GroupTable.TITLE}, ''),
|
NULLIF(${GroupTable.TABLE_NAME}.${GroupTable.TITLE}, ''),
|
||||||
|
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.NICKNAME_JOINED_NAME}, ''),
|
||||||
|
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.NICKNAME_GIVEN_NAME}, ''),
|
||||||
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_JOINED_NAME}, ''),
|
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_JOINED_NAME}, ''),
|
||||||
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_GIVEN_NAME}, ''),
|
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.SYSTEM_GIVEN_NAME}, ''),
|
||||||
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_JOINED_NAME}, ''),
|
NULLIF(${RecipientTable.TABLE_NAME}.${RecipientTable.PROFILE_JOINED_NAME}, ''),
|
||||||
|
@ -1141,7 +1239,7 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
FROM (
|
FROM (
|
||||||
WITH cte AS (
|
WITH cte AS (
|
||||||
SELECT
|
SELECT
|
||||||
$ID, $TIMESTAMP, $EVENT, $DIRECTION, $PEER, $TYPE, $CALL_ID, $MESSAGE_ID, $RINGER,
|
$ID, $TIMESTAMP, $EVENT, $DIRECTION, $PEER, $TYPE, $CALL_ID, $MESSAGE_ID, $RINGER, $LOCAL_JOINED, $GROUP_CALL_ACTIVE,
|
||||||
(
|
(
|
||||||
SELECT
|
SELECT
|
||||||
$ID
|
$ID
|
||||||
|
@ -1225,10 +1323,20 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markRingingCallsAsMissed() {
|
fun markRingingCallsAsMissed() {
|
||||||
writableDatabase.update(TABLE_NAME)
|
writableDatabase.withinTransaction { db ->
|
||||||
|
val messageIds: List<Long> = db.select(MESSAGE_ID)
|
||||||
|
.from(TABLE_NAME)
|
||||||
|
.where("$EVENT = ? AND $MESSAGE_ID != NULL", Event.serialize(Event.RINGING))
|
||||||
|
.run()
|
||||||
|
.readToList { it.requireLong(MESSAGE_ID) }
|
||||||
|
|
||||||
|
db.update(TABLE_NAME)
|
||||||
.values(EVENT to Event.serialize(Event.MISSED))
|
.values(EVENT to Event.serialize(Event.MISSED))
|
||||||
.where("$EVENT = ?", Event.serialize(Event.RINGING))
|
.where("$EVENT = ?", Event.serialize(Event.RINGING))
|
||||||
.run()
|
.run()
|
||||||
|
|
||||||
|
SignalDatabase.messages.clearIsRingingOnLocalDeviceFlag(messageIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCallsCount(searchTerm: String?, filter: CallLogFilter): Int {
|
fun getCallsCount(searchTerm: String?, filter: CallLogFilter): Int {
|
||||||
|
@ -1288,6 +1396,10 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param isGroupCallActive - Whether the group call currently contains users. Only valid for group calls.
|
||||||
|
* @param didLocalUserJoin - Determines whether the local user joined this call. Only valid for group calls.
|
||||||
|
*/
|
||||||
data class Call(
|
data class Call(
|
||||||
val callId: Long,
|
val callId: Long,
|
||||||
val peer: RecipientId,
|
val peer: RecipientId,
|
||||||
|
@ -1296,11 +1408,20 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
val event: Event,
|
val event: Event,
|
||||||
val messageId: Long?,
|
val messageId: Long?,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val ringerRecipient: RecipientId?
|
val ringerRecipient: RecipientId?,
|
||||||
|
val isGroupCallActive: Boolean,
|
||||||
|
val didLocalUserJoin: Boolean
|
||||||
) {
|
) {
|
||||||
val messageType: Long = getMessageType(type, direction, event)
|
val messageType: Long = getMessageType(type, direction, event)
|
||||||
|
|
||||||
|
val isDisplayedAsMissedCallInUi = isDisplayedAsMissedCallInUi(this)
|
||||||
|
|
||||||
companion object Deserializer : Serializer<Call, Cursor> {
|
companion object Deserializer : Serializer<Call, Cursor> {
|
||||||
|
|
||||||
|
private fun isDisplayedAsMissedCallInUi(call: Call): Boolean {
|
||||||
|
return call.event in Event.DISPLAY_AS_MISSED_CALL || (call.event == Event.GENERIC_GROUP_CALL && !call.didLocalUserJoin && !call.isGroupCallActive)
|
||||||
|
}
|
||||||
|
|
||||||
fun getMessageType(type: Type, direction: Direction, event: Event): Long {
|
fun getMessageType(type: Type, direction: Direction, event: Event): Long {
|
||||||
if (type == Type.GROUP_CALL || type == Type.AD_HOC_CALL) {
|
if (type == Type.GROUP_CALL || type == Type.AD_HOC_CALL) {
|
||||||
return MessageTypes.GROUP_CALL_TYPE
|
return MessageTypes.GROUP_CALL_TYPE
|
||||||
|
@ -1334,7 +1455,9 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
isGroupCallActive = data.requireBoolean(GROUP_CALL_ACTIVE),
|
||||||
|
didLocalUserJoin = data.requireBoolean(LOCAL_JOINED)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1482,6 +1605,14 @@ class CallTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTabl
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object Serializer : IntSerializer<Event> {
|
companion object Serializer : IntSerializer<Event> {
|
||||||
|
|
||||||
|
val DISPLAY_AS_MISSED_CALL = listOf(
|
||||||
|
MISSED,
|
||||||
|
MISSED_NOTIFICATION_PROFILE,
|
||||||
|
DECLINED,
|
||||||
|
NOT_ACCEPTED
|
||||||
|
)
|
||||||
|
|
||||||
override fun serialize(data: Event): Int = data.code
|
override fun serialize(data: Event): Int = data.code
|
||||||
|
|
||||||
override fun deserialize(data: Int): Event {
|
override fun deserialize(data: Int): Event {
|
||||||
|
|
|
@ -828,7 +828,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||||
timestamp: Long,
|
timestamp: Long,
|
||||||
eraId: String,
|
eraId: String,
|
||||||
joinedUuids: Collection<UUID>,
|
joinedUuids: Collection<UUID>,
|
||||||
isCallFull: Boolean
|
isCallFull: Boolean,
|
||||||
|
isIncomingGroupCallRingingOnLocalDevice: Boolean
|
||||||
): MessageId {
|
): MessageId {
|
||||||
val recipient = Recipient.resolved(groupRecipientId)
|
val recipient = Recipient.resolved(groupRecipientId)
|
||||||
val threadId = threads.getOrCreateThreadIdFor(recipient)
|
val threadId = threads.getOrCreateThreadIdFor(recipient)
|
||||||
|
@ -840,7 +841,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||||
startedCallUuid = Recipient.resolved(sender).requireServiceId().toString(),
|
startedCallUuid = Recipient.resolved(sender).requireServiceId().toString(),
|
||||||
startedCallTimestamp = timestamp,
|
startedCallTimestamp = timestamp,
|
||||||
inCallUuids = joinedUuids.map { it.toString() },
|
inCallUuids = joinedUuids.map { it.toString() },
|
||||||
isCallFull = isCallFull
|
isCallFull = isCallFull,
|
||||||
|
localUserJoined = joinedUuids.contains(Recipient.self().requireServiceId().rawUuid),
|
||||||
|
isRingingOnLocalDevice = isIncomingGroupCallRingingOnLocalDevice
|
||||||
).encode()
|
).encode()
|
||||||
|
|
||||||
val values = contentValuesOf(
|
val values = contentValuesOf(
|
||||||
|
@ -893,11 +896,46 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the flag in GroupCallUpdateDetailsUtil that specifies that the call is ringing on the local device.
|
||||||
|
* Called when cleaning up the call ringing state (which can get out of sync in the case of an application crash)
|
||||||
|
*/
|
||||||
|
fun clearIsRingingOnLocalDeviceFlag(messageIds: Collection<Long>) {
|
||||||
|
writableDatabase.withinTransaction { db ->
|
||||||
|
val queries = SqlUtil.buildCollectionQuery(ID, messageIds)
|
||||||
|
|
||||||
|
for (query in queries) {
|
||||||
|
val messageIdBodyPairs = db.select(ID, BODY)
|
||||||
|
.from(TABLE_NAME)
|
||||||
|
.where(query.where, query.whereArgs)
|
||||||
|
.run()
|
||||||
|
.readToList { cursor ->
|
||||||
|
cursor.requireLong(ID) to cursor.requireString(BODY)
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((messageId, body) in messageIdBodyPairs) {
|
||||||
|
val oldBody = GroupCallUpdateDetailsUtil.parse(body)
|
||||||
|
if (!oldBody.isRingingOnLocalDevice) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBody = GroupCallUpdateDetailsUtil.createUpdatedBody(oldBody, oldBody.inCallUuids, oldBody.isCallFull, false)
|
||||||
|
|
||||||
|
db.update(TABLE_NAME)
|
||||||
|
.values(BODY to newBody)
|
||||||
|
.where(ID_WHERE, messageId)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateGroupCall(
|
fun updateGroupCall(
|
||||||
messageId: Long,
|
messageId: Long,
|
||||||
eraId: String,
|
eraId: String,
|
||||||
joinedUuids: Collection<UUID>,
|
joinedUuids: Collection<UUID>,
|
||||||
isCallFull: Boolean
|
isCallFull: Boolean,
|
||||||
|
isRingingOnLocalDevice: Boolean
|
||||||
): MessageId {
|
): MessageId {
|
||||||
writableDatabase.withinTransaction { db ->
|
writableDatabase.withinTransaction { db ->
|
||||||
val message = try {
|
val message = try {
|
||||||
|
@ -911,7 +949,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||||
val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId)
|
val sameEraId = updateDetail.eraId == eraId && !Util.isEmpty(eraId)
|
||||||
val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList()
|
val inCallUuids = if (sameEraId) joinedUuids.map { it.toString() } else emptyList()
|
||||||
val contentValues = contentValuesOf(
|
val contentValues = contentValuesOf(
|
||||||
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull)
|
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(updateDetail, inCallUuids, isCallFull, isRingingOnLocalDevice)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (sameEraId && containsSelf) {
|
if (sameEraId && containsSelf) {
|
||||||
|
@ -929,7 +967,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||||
return MessageId(messageId)
|
return MessageId(messageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePreviousGroupCall(threadId: Long, peekGroupCallEraId: String?, peekJoinedUuids: Collection<UUID>, isCallFull: Boolean): Boolean {
|
fun updatePreviousGroupCall(
|
||||||
|
threadId: Long,
|
||||||
|
peekGroupCallEraId: String?,
|
||||||
|
peekJoinedUuids: Collection<UUID>,
|
||||||
|
isCallFull: Boolean,
|
||||||
|
isRingingOnLocalDevice: Boolean
|
||||||
|
): Boolean {
|
||||||
return writableDatabase.withinTransaction { db ->
|
return writableDatabase.withinTransaction { db ->
|
||||||
val cursor = db
|
val cursor = db
|
||||||
.select(*MMS_PROJECTION)
|
.select(*MMS_PROJECTION)
|
||||||
|
@ -952,7 +996,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||||
}
|
}
|
||||||
|
|
||||||
val contentValues = contentValuesOf(
|
val contentValues = contentValuesOf(
|
||||||
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull)
|
BODY to GroupCallUpdateDetailsUtil.createUpdatedBody(groupCallUpdateDetails, inCallUuids, isCallFull, isRingingOnLocalDevice)
|
||||||
)
|
)
|
||||||
|
|
||||||
if (sameEraId && containsSelf) {
|
if (sameEraId && containsSelf) {
|
||||||
|
|
|
@ -136,24 +136,21 @@ public final class ThreadBodyUtil {
|
||||||
boolean accepted = call.getEvent() == CallTable.Event.ACCEPTED;
|
boolean accepted = call.getEvent() == CallTable.Event.ACCEPTED;
|
||||||
if (call.getDirection() == CallTable.Direction.OUTGOING) {
|
if (call.getDirection() == CallTable.Direction.OUTGOING) {
|
||||||
if (call.getType() == CallTable.Type.AUDIO_CALL) {
|
if (call.getType() == CallTable.Type.AUDIO_CALL) {
|
||||||
return context.getString(accepted ? R.string.MessageRecord_outgoing_voice_call : R.string.MessageRecord_unanswered_voice_call);
|
return context.getString(R.string.MessageRecord_outgoing_voice_call);
|
||||||
} else {
|
} else {
|
||||||
return context.getString(accepted ? R.string.MessageRecord_outgoing_video_call : R.string.MessageRecord_unanswered_video_call);
|
return context.getString(R.string.MessageRecord_outgoing_video_call);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
|
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
|
||||||
boolean isMissed = call.getEvent().isMissedCall();
|
|
||||||
|
|
||||||
if (accepted) {
|
if (accepted || !call.isDisplayedAsMissedCallInUi()) {
|
||||||
return context.getString(isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call);
|
return context.getString(isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call);
|
||||||
} else if (isMissed) {
|
} else {
|
||||||
if (call.getEvent() == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
|
if (call.getEvent() == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
|
||||||
return isVideoCall ? context.getString(R.string.MessageRecord_missed_video_call_notification_profile) : context.getString(R.string.MessageRecord_missed_voice_call_notification_profile);
|
return isVideoCall ? context.getString(R.string.MessageRecord_missed_video_call_notification_profile) : context.getString(R.string.MessageRecord_missed_voice_call_notification_profile);
|
||||||
} else {
|
} else {
|
||||||
return isVideoCall ? context.getString(R.string.MessageRecord_missed_video_call) : context.getString(R.string.MessageRecord_missed_voice_call);
|
return isVideoCall ? context.getString(R.string.MessageRecord_missed_video_call) : context.getString(R.string.MessageRecord_missed_voice_call);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return isVideoCall ? context.getString(R.string.MessageRecord_you_declined_a_video_call) : context.getString(R.string.MessageRecord_you_declined_a_voice_call);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -82,6 +82,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V221_AddReadColumnT
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V222_DataHashRefactor
|
import org.thoughtcrime.securesms.database.helpers.migration.V222_DataHashRefactor
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V223_AddNicknameAndNoteFieldsToRecipientTable
|
import org.thoughtcrime.securesms.database.helpers.migration.V223_AddNicknameAndNoteFieldsToRecipientTable
|
||||||
import org.thoughtcrime.securesms.database.helpers.migration.V224_AddAttachmentArchiveColumns
|
import org.thoughtcrime.securesms.database.helpers.migration.V224_AddAttachmentArchiveColumns
|
||||||
|
import org.thoughtcrime.securesms.database.helpers.migration.V225_AddLocalUserJoinedStateAndGroupCallActiveState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||||
|
@ -166,10 +167,11 @@ object SignalDatabaseMigrations {
|
||||||
221 to V221_AddReadColumnToCallEventsTable,
|
221 to V221_AddReadColumnToCallEventsTable,
|
||||||
222 to V222_DataHashRefactor,
|
222 to V222_DataHashRefactor,
|
||||||
223 to V223_AddNicknameAndNoteFieldsToRecipientTable,
|
223 to V223_AddNicknameAndNoteFieldsToRecipientTable,
|
||||||
224 to V224_AddAttachmentArchiveColumns
|
224 to V224_AddAttachmentArchiveColumns,
|
||||||
|
225 to V225_AddLocalUserJoinedStateAndGroupCallActiveState
|
||||||
)
|
)
|
||||||
|
|
||||||
const val DATABASE_VERSION = 224
|
const val DATABASE_VERSION = 225
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
|
|
|
@ -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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.signal.core.util.logging.Log;
|
|
||||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
|
||||||
import org.signal.core.util.Base64;
|
import org.signal.core.util.Base64;
|
||||||
|
import org.signal.core.util.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate;
|
||||||
|
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class GroupCallUpdateDetailsUtil {
|
public final class GroupCallUpdateDetailsUtil {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(GroupCallUpdateDetailsUtil.class);
|
private static final String TAG = Log.tag(GroupCallUpdateDetailsUtil.class);
|
||||||
|
|
||||||
|
private static final long CALL_RECENCY_TIMEOUT = TimeUnit.MINUTES.toMillis(5);
|
||||||
|
|
||||||
private GroupCallUpdateDetailsUtil() {
|
private GroupCallUpdateDetailsUtil() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a group chat update message body from backup data
|
||||||
|
*/
|
||||||
|
public static @NonNull String createBodyFromBackup(@NonNull GroupCallChatUpdate groupCallChatUpdate) {
|
||||||
|
ServiceId.ACI startedCall = groupCallChatUpdate.startedCallAci != null ? ServiceId.ACI.parseOrNull(groupCallChatUpdate.startedCallAci) : null;
|
||||||
|
|
||||||
|
GroupCallUpdateDetails details = new GroupCallUpdateDetails.Builder()
|
||||||
|
.startedCallUuid(Objects.toString(startedCall, null))
|
||||||
|
.startedCallTimestamp(groupCallChatUpdate.startedCallTimestamp)
|
||||||
|
.endedCallTimestamp(groupCallChatUpdate.endedCallTimestamp)
|
||||||
|
.isCallFull(false)
|
||||||
|
.inCallUuids(groupCallChatUpdate.inCallAcis.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(ServiceId.ACI::parseOrNull)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(ServiceId.ACI::toString)
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
)
|
||||||
|
.isRingingOnLocalDevice(false)
|
||||||
|
.localUserJoined(groupCallChatUpdate.localUserJoined != GroupCallChatUpdate.LocalUserJoined.DID_NOT_JOIN)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return Base64.encodeWithPadding(details.encode());
|
||||||
|
}
|
||||||
|
|
||||||
public static @NonNull GroupCallUpdateDetails parse(@Nullable String body) {
|
public static @NonNull GroupCallUpdateDetails parse(@Nullable String body) {
|
||||||
GroupCallUpdateDetails groupCallUpdateDetails = new GroupCallUpdateDetails();
|
GroupCallUpdateDetails groupCallUpdateDetails = new GroupCallUpdateDetails();
|
||||||
|
|
||||||
|
@ -33,10 +66,38 @@ public final class GroupCallUpdateDetailsUtil {
|
||||||
return groupCallUpdateDetails;
|
return groupCallUpdateDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static @NonNull String createUpdatedBody(@NonNull GroupCallUpdateDetails groupCallUpdateDetails, @NonNull List<String> inCallUuids, boolean isCallFull) {
|
public static boolean checkCallEndedRecently(@NonNull GroupCallUpdateDetails groupCallUpdateDetails) {
|
||||||
|
if (groupCallUpdateDetails.endedCallTimestamp == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now > groupCallUpdateDetails.endedCallTimestamp) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return now - groupCallUpdateDetails.endedCallTimestamp < CALL_RECENCY_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static @NonNull String createUpdatedBody(@NonNull GroupCallUpdateDetails groupCallUpdateDetails, @NonNull List<String> inCallUuids, boolean isCallFull, boolean isRingingOnLocalDevice)
|
||||||
|
{
|
||||||
|
boolean localUserJoined = groupCallUpdateDetails.localUserJoined || inCallUuids.contains(Recipient.self().requireServiceId().getRawUuid().toString());
|
||||||
|
long endedTimestamp = groupCallUpdateDetails.endedCallTimestamp;
|
||||||
|
boolean callBecameEmpty = !groupCallUpdateDetails.inCallUuids.isEmpty() && inCallUuids.isEmpty() && !isRingingOnLocalDevice;
|
||||||
|
boolean ringTerminatedWithNoUsers = groupCallUpdateDetails.isRingingOnLocalDevice && !isRingingOnLocalDevice && inCallUuids.isEmpty();
|
||||||
|
|
||||||
|
if (callBecameEmpty || ringTerminatedWithNoUsers) {
|
||||||
|
endedTimestamp = System.currentTimeMillis();
|
||||||
|
} else if (!inCallUuids.isEmpty()) {
|
||||||
|
endedTimestamp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
GroupCallUpdateDetails.Builder builder = groupCallUpdateDetails.newBuilder()
|
GroupCallUpdateDetails.Builder builder = groupCallUpdateDetails.newBuilder()
|
||||||
.isCallFull(isCallFull)
|
.isCallFull(isCallFull)
|
||||||
.inCallUuids(inCallUuids);
|
.inCallUuids(inCallUuids)
|
||||||
|
.localUserJoined(localUserJoined)
|
||||||
|
.endedCallTimestamp(endedTimestamp)
|
||||||
|
.isRingingOnLocalDevice(isRingingOnLocalDevice);
|
||||||
|
|
||||||
return Base64.encodeWithPadding(builder.build().encode());
|
return Base64.encodeWithPadding(builder.build().encode());
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a group call update message based on time and joined members.
|
* Create a group call update message based on time and joined members.
|
||||||
|
@ -53,45 +54,61 @@ public class GroupCallUpdateMessageFactory implements UpdateDescription.Spannabl
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NonNull String createString() {
|
private @NonNull String createString() {
|
||||||
|
long endedTimestamp = groupCallUpdateDetails.endedCallTimestamp;
|
||||||
|
boolean isWithinTimeout = GroupCallUpdateDetailsUtil.checkCallEndedRecently(groupCallUpdateDetails);
|
||||||
String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.startedCallTimestamp);
|
String time = DateUtils.getTimeString(context, Locale.getDefault(), groupCallUpdateDetails.startedCallTimestamp);
|
||||||
|
boolean isOutgoing = Objects.equals(selfAci.toString(), groupCallUpdateDetails.startedCallUuid);
|
||||||
|
|
||||||
switch (joinedMembers.size()) {
|
switch (joinedMembers.size()) {
|
||||||
case 0:
|
case 0:
|
||||||
return withTime ? context.getString(R.string.MessageRecord_group_call_s, time)
|
if (isWithinTimeout) {
|
||||||
: context.getString(R.string.MessageRecord_group_call);
|
return withTime ? context.getString(R.string.MessageRecord__the_video_call_has_ended_s, time)
|
||||||
|
: context.getString(R.string.MessageRecord__the_video_call_has_ended);
|
||||||
|
} else if (endedTimestamp == 0 || groupCallUpdateDetails.localUserJoined) {
|
||||||
|
if (isOutgoing) {
|
||||||
|
return withTime ? context.getString(R.string.MessageRecord__outgoing_video_call_s, time)
|
||||||
|
: context.getString(R.string.MessageRecord__outgoing_video_call);
|
||||||
|
} else {
|
||||||
|
return withTime ? context.getString(R.string.MessageRecord__incoming_video_call_s, time)
|
||||||
|
: context.getString(R.string.MessageRecord__incoming_video_call);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return withTime ? context.getString(R.string.MessageRecord__missed_video_call_s, time)
|
||||||
|
: context.getString(R.string.MessageRecord__missed_video_call);
|
||||||
|
}
|
||||||
case 1:
|
case 1:
|
||||||
if (joinedMembers.get(0).toString().equals(groupCallUpdateDetails.startedCallUuid)) {
|
if (joinedMembers.get(0).toString().equals(groupCallUpdateDetails.startedCallUuid)) {
|
||||||
if (Objects.equals(joinedMembers.get(0), selfAci)) {
|
if (Objects.equals(joinedMembers.get(0), selfAci)) {
|
||||||
return withTime ? context.getString(R.string.MessageRecord_you_started_a_group_call_s, time)
|
return withTime ? context.getString(R.string.MessageRecord__you_started_a_video_call_s, time)
|
||||||
: context.getString(R.string.MessageRecord_you_started_a_group_call);
|
: context.getString(R.string.MessageRecord__you_started_a_video_call);
|
||||||
} else {
|
} else {
|
||||||
return withTime ? context.getString(R.string.MessageRecord_s_started_a_group_call_s, describe(joinedMembers.get(0)), time)
|
return withTime ? context.getString(R.string.MessageRecord__s_started_a_video_call_s, describe(joinedMembers.get(0)), time)
|
||||||
: context.getString(R.string.MessageRecord_s_started_a_group_call, describe(joinedMembers.get(0)));
|
: context.getString(R.string.MessageRecord__s_started_a_video_call, describe(joinedMembers.get(0)));
|
||||||
}
|
}
|
||||||
} else if (Objects.equals(joinedMembers.get(0), selfAci)) {
|
} else if (Objects.equals(joinedMembers.get(0), selfAci)) {
|
||||||
return withTime ? context.getString(R.string.MessageRecord_you_are_in_the_group_call_s1, time)
|
return withTime ? context.getString(R.string.MessageRecord_you_are_in_the_call_s1, time)
|
||||||
: context.getString(R.string.MessageRecord_you_are_in_the_group_call);
|
: context.getString(R.string.MessageRecord_you_are_in_the_call);
|
||||||
} else {
|
} else {
|
||||||
return withTime ? context.getString(R.string.MessageRecord_s_is_in_the_group_call_s, describe(joinedMembers.get(0)), time)
|
return withTime ? context.getString(R.string.MessageRecord_s_is_in_the_call_s, describe(joinedMembers.get(0)), time)
|
||||||
: context.getString(R.string.MessageRecord_s_is_in_the_group_call, describe(joinedMembers.get(0)));
|
: context.getString(R.string.MessageRecord_s_is_in_the_call, describe(joinedMembers.get(0)));
|
||||||
}
|
}
|
||||||
case 2:
|
case 2:
|
||||||
return withTime ? context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call_s1,
|
return withTime ? context.getString(R.string.MessageRecord_s_and_s_are_in_the_call_s1,
|
||||||
describe(joinedMembers.get(0)),
|
describe(joinedMembers.get(0)),
|
||||||
describe(joinedMembers.get(1)),
|
describe(joinedMembers.get(1)),
|
||||||
time)
|
time)
|
||||||
: context.getString(R.string.MessageRecord_s_and_s_are_in_the_group_call,
|
: context.getString(R.string.MessageRecord_s_and_s_are_in_the_call,
|
||||||
describe(joinedMembers.get(0)),
|
describe(joinedMembers.get(0)),
|
||||||
describe(joinedMembers.get(1)));
|
describe(joinedMembers.get(1)));
|
||||||
default:
|
default:
|
||||||
int others = joinedMembers.size() - 2;
|
int others = joinedMembers.size() - 2;
|
||||||
return withTime ? context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call_s,
|
return withTime ? context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_call_s,
|
||||||
others,
|
others,
|
||||||
describe(joinedMembers.get(0)),
|
describe(joinedMembers.get(0)),
|
||||||
describe(joinedMembers.get(1)),
|
describe(joinedMembers.get(1)),
|
||||||
others,
|
others,
|
||||||
time)
|
time)
|
||||||
: context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_group_call,
|
: context.getResources().getQuantityString(R.plurals.MessageRecord_s_s_and_d_others_are_in_the_call,
|
||||||
others,
|
others,
|
||||||
describe(joinedMembers.get(0)),
|
describe(joinedMembers.get(0)),
|
||||||
describe(joinedMembers.get(1)),
|
describe(joinedMembers.get(1)),
|
||||||
|
|
|
@ -232,21 +232,20 @@ public class MmsMessageRecord extends MessageRecord {
|
||||||
|
|
||||||
if (call.getDirection() == CallTable.Direction.OUTGOING) {
|
if (call.getDirection() == CallTable.Direction.OUTGOING) {
|
||||||
if (call.getType() == CallTable.Type.AUDIO_CALL) {
|
if (call.getType() == CallTable.Type.AUDIO_CALL) {
|
||||||
int updateString = accepted ? R.string.MessageRecord_outgoing_voice_call : R.string.MessageRecord_unanswered_voice_call;
|
int updateString = R.string.MessageRecord_outgoing_voice_call;
|
||||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_audio_call_outgoing_16);
|
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_audio_call_outgoing_16);
|
||||||
} else {
|
} else {
|
||||||
int updateString = accepted ? R.string.MessageRecord_outgoing_video_call : R.string.MessageRecord_unanswered_video_call;
|
int updateString = R.string.MessageRecord_outgoing_video_call;
|
||||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_video_call_outgoing_16);
|
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), R.drawable.ic_update_video_call_outgoing_16);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
|
boolean isVideoCall = call.getType() == CallTable.Type.VIDEO_CALL;
|
||||||
boolean isMissed = call.getEvent().isMissedCall();
|
|
||||||
|
|
||||||
if (accepted) {
|
if (accepted || !call.isDisplayedAsMissedCallInUi()) {
|
||||||
int updateString = isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call;
|
int updateString = isVideoCall ? R.string.MessageRecord_incoming_video_call : R.string.MessageRecord_incoming_voice_call;
|
||||||
int icon = isVideoCall ? R.drawable.ic_update_video_call_incoming_16 : R.drawable.ic_update_audio_call_incoming_16;
|
int icon = isVideoCall ? R.drawable.ic_update_video_call_incoming_16 : R.drawable.ic_update_audio_call_incoming_16;
|
||||||
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
|
return staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(updateString), callDateString), icon);
|
||||||
} else if (isMissed) {
|
} else {
|
||||||
int icon = isVideoCall ? R.drawable.ic_update_video_call_missed_16 : R.drawable.ic_update_audio_call_missed_16;
|
int icon = isVideoCall ? R.drawable.ic_update_video_call_missed_16 : R.drawable.ic_update_audio_call_missed_16;
|
||||||
int message;
|
int message;
|
||||||
if (call.getEvent() == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
|
if (call.getEvent() == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
|
||||||
|
@ -261,9 +260,6 @@ public class MmsMessageRecord extends MessageRecord {
|
||||||
icon,
|
icon,
|
||||||
ContextCompat.getColor(context, R.color.core_red_shade),
|
ContextCompat.getColor(context, R.color.core_red_shade),
|
||||||
ContextCompat.getColor(context, R.color.core_red));
|
ContextCompat.getColor(context, R.color.core_red));
|
||||||
} else {
|
|
||||||
return isVideoCall ? staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_you_declined_a_video_call), callDateString), R.drawable.ic_update_video_call_incoming_16)
|
|
||||||
: staticUpdateDescription(context.getString(R.string.MessageRecord_call_message_with_date, context.getString(R.string.MessageRecord_you_declined_a_voice_call), callDateString), R.drawable.ic_update_audio_call_incoming_16);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -516,9 +516,17 @@ message IndividualCallChatUpdate {
|
||||||
}
|
}
|
||||||
|
|
||||||
message GroupCallChatUpdate {
|
message GroupCallChatUpdate {
|
||||||
|
enum LocalUserJoined {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
JOINED = 1;
|
||||||
|
DID_NOT_JOIN = 2;
|
||||||
|
}
|
||||||
|
|
||||||
optional bytes startedCallAci = 1;
|
optional bytes startedCallAci = 1;
|
||||||
uint64 startedCallTimestamp = 2;
|
uint64 startedCallTimestamp = 2;
|
||||||
repeated bytes inCallAcis = 3;
|
repeated bytes inCallAcis = 3;
|
||||||
|
uint64 endedCallTimestamp = 4; // 0 indicates we do not know
|
||||||
|
LocalUserJoined localUserJoined = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SimpleChatUpdate {
|
message SimpleChatUpdate {
|
||||||
|
|
|
@ -121,6 +121,9 @@ message GroupCallUpdateDetails {
|
||||||
int64 startedCallTimestamp = 3;
|
int64 startedCallTimestamp = 3;
|
||||||
repeated string inCallUuids = 4;
|
repeated string inCallUuids = 4;
|
||||||
bool isCallFull = 5;
|
bool isCallFull = 5;
|
||||||
|
bool localUserJoined = 6;
|
||||||
|
int64 endedCallTimestamp = 7;
|
||||||
|
bool isRingingOnLocalDevice = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ExpiringProfileKeyCredentialColumnData {
|
message ExpiringProfileKeyCredentialColumnData {
|
||||||
|
|
|
@ -1335,17 +1335,13 @@
|
||||||
<string name="MessageRecord_left_group">You have left the group.</string>
|
<string name="MessageRecord_left_group">You have left the group.</string>
|
||||||
<string name="MessageRecord_you_updated_group">You updated the group.</string>
|
<string name="MessageRecord_you_updated_group">You updated the group.</string>
|
||||||
<string name="MessageRecord_the_group_was_updated">The group was updated.</string>
|
<string name="MessageRecord_the_group_was_updated">The group was updated.</string>
|
||||||
<!-- Update message shown when placing an outgoing 1:1 voice/audio call and it\'s answered by the other party -->
|
<!-- Update message shown when placing an outgoing 1:1 voice/audio call -->
|
||||||
<string name="MessageRecord_outgoing_voice_call">Outgoing voice call</string>
|
<string name="MessageRecord_outgoing_voice_call">Outgoing voice call</string>
|
||||||
<!-- Update message shown when placing an outgoing 1:1 video call and it\'s answered by the other party -->
|
<!-- Update message shown when placing an outgoing 1:1 video call -->
|
||||||
<string name="MessageRecord_outgoing_video_call">Outgoing video call</string>
|
<string name="MessageRecord_outgoing_video_call">Outgoing video call</string>
|
||||||
<!-- Update message shown when placing an outgoing 1:1 voice/audio call and it\'s not answered by the other party -->
|
<!-- Update message shown when receiving an incoming 1:1 voice/audio call and it\'s answered or ringing -->
|
||||||
<string name="MessageRecord_unanswered_voice_call">Unanswered voice call</string>
|
|
||||||
<!-- Update message shown when placing an outgoing 1:1 video call and it\'s not answered by the other party -->
|
|
||||||
<string name="MessageRecord_unanswered_video_call">Unanswered video call</string>
|
|
||||||
<!-- Update message shown when receiving an incoming 1:1 voice/audio call and it\'s answered -->
|
|
||||||
<string name="MessageRecord_incoming_voice_call">Incoming voice call</string>
|
<string name="MessageRecord_incoming_voice_call">Incoming voice call</string>
|
||||||
<!-- Update message shown when receiving an incoming 1:1 video call and answered -->
|
<!-- Update message shown when receiving an incoming 1:1 video call and answered or ringing -->
|
||||||
<string name="MessageRecord_incoming_video_call">Incoming video call</string>
|
<string name="MessageRecord_incoming_video_call">Incoming video call</string>
|
||||||
<!-- Update message shown when receiving an incoming 1:1 voice/audio call and not answered -->
|
<!-- Update message shown when receiving an incoming 1:1 voice/audio call and not answered -->
|
||||||
<string name="MessageRecord_missed_voice_call">Missed voice call</string>
|
<string name="MessageRecord_missed_voice_call">Missed voice call</string>
|
||||||
|
@ -1355,10 +1351,6 @@
|
||||||
<string name="MessageRecord_missed_voice_call_notification_profile">Missed voice call while notification profile on</string>
|
<string name="MessageRecord_missed_voice_call_notification_profile">Missed voice call while notification profile on</string>
|
||||||
<!-- Update message shown when receiving an incoming video call and declined due to notification profile -->
|
<!-- Update message shown when receiving an incoming video call and declined due to notification profile -->
|
||||||
<string name="MessageRecord_missed_video_call_notification_profile">Missed video call while notification profile on</string>
|
<string name="MessageRecord_missed_video_call_notification_profile">Missed video call while notification profile on</string>
|
||||||
<!-- Update message shown when receiving an incoming 1:1 voice/audio call and explicitly declined -->
|
|
||||||
<string name="MessageRecord_you_declined_a_voice_call">You declined a voice call</string>
|
|
||||||
<!-- Update message shown when receiving an incoming 1:1 video call and explicitly declined -->
|
|
||||||
<string name="MessageRecord_you_declined_a_video_call">You declined a video call</string>
|
|
||||||
<!-- Call update formatter string to place the update message next to a time stamp. e.g., \'Incoming voice call · 11:11am\' -->
|
<!-- Call update formatter string to place the update message next to a time stamp. e.g., \'Incoming voice call · 11:11am\' -->
|
||||||
<string name="MessageRecord_call_message_with_date">%1$s · %2$s</string>
|
<string name="MessageRecord_call_message_with_date">%1$s · %2$s</string>
|
||||||
<string name="MessageRecord_s_updated_group">%s updated the group.</string>
|
<string name="MessageRecord_s_updated_group">%s updated the group.</string>
|
||||||
|
@ -1567,28 +1559,53 @@
|
||||||
<string name="MessageRecord_can_accept_payments">%s can now accept Payments</string>
|
<string name="MessageRecord_can_accept_payments">%s can now accept Payments</string>
|
||||||
|
|
||||||
<!-- Group Calling update messages -->
|
<!-- Group Calling update messages -->
|
||||||
<string name="MessageRecord_s_started_a_group_call_s">%1$s started a group call · %2$s</string>
|
<!-- Chat log text for an ongoing group call that has one participant with a placeholder for the short display name of the user that joined and a placeholder for formatted time -->
|
||||||
<string name="MessageRecord_you_started_a_group_call_s">You started a group call · %1$s</string>
|
<string name="MessageRecord_s_is_in_the_call_s">%1$s is in the call · %2$s</string>
|
||||||
<string name="MessageRecord_s_is_in_the_group_call_s">%1$s is in the group call · %2$s</string>
|
<!-- Chat log text for an ongoing group call only the local user has joined with a placeholder for formatted time -->
|
||||||
<string name="MessageRecord_you_are_in_the_group_call_s1">You are in the group call · %1$s</string>
|
<string name="MessageRecord_you_are_in_the_call_s1">You are in the call · %1$s</string>
|
||||||
<string name="MessageRecord_s_and_s_are_in_the_group_call_s1">%1$s and %2$s are in the group call · %3$s</string>
|
<!-- Chat log text for an ongoing group call with two participants with two placeholders for the short display name of the users that joined and a placeholder for formatted time -->
|
||||||
<string name="MessageRecord_group_call_s">Group call · %1$s</string>
|
<string name="MessageRecord_s_and_s_are_in_the_call_s1">%1$s and %2$s are in the call · %3$s</string>
|
||||||
|
<!-- Chat log text for an ongoing group call that has one participant -->
|
||||||
<string name="MessageRecord_s_started_a_group_call">%1$s started a group call</string>
|
<string name="MessageRecord_s_is_in_the_call">%1$s is in the call</string>
|
||||||
<string name="MessageRecord_you_started_a_group_call">You started a group call</string>
|
<!-- Chat log text for an ongoing group call only the local user has joined -->
|
||||||
<string name="MessageRecord_s_is_in_the_group_call">%1$s is in the group call</string>
|
<string name="MessageRecord_you_are_in_the_call">You are in the call</string>
|
||||||
<string name="MessageRecord_you_are_in_the_group_call">You are in the group call</string>
|
<!-- Chat log text for an ongoing group call with two participants with two placeholders, each for the short display name of the users that joined -->
|
||||||
<string name="MessageRecord_s_and_s_are_in_the_group_call">%1$s and %2$s are in the group call</string>
|
<string name="MessageRecord_s_and_s_are_in_the_call">%1$s and %2$s are in the call</string>
|
||||||
<string name="MessageRecord_group_call">Group call</string>
|
<!-- Chat log text for a group call that ended within the last 5 minutes -->
|
||||||
|
<string name="MessageRecord__the_video_call_has_ended">The video call has ended</string>
|
||||||
|
<!-- Chat log text for a group call that ended within the last 5 minutes with a placeholder for formatted time -->
|
||||||
|
<string name="MessageRecord__the_video_call_has_ended_s">The video call has ended · %1$s</string>
|
||||||
|
<!-- Chat log text for a group call that ended more than 5 minutes ago and was missed -->
|
||||||
|
<string name="MessageRecord__missed_video_call">Missed video call</string>
|
||||||
|
<!-- Chat log text for a group call that ended more than 5 minutes ago and was missed with a placeholder for formatted time -->
|
||||||
|
<string name="MessageRecord__missed_video_call_s">Missed video call · %1$s</string>
|
||||||
|
<!-- Chat log text for a group call that ended that the local user did not start -->
|
||||||
|
<string name="MessageRecord__incoming_video_call">Incoming video call</string>
|
||||||
|
<!-- Chat log text for a group call that ended that the local user did not start with a placeholder for formatted time -->
|
||||||
|
<string name="MessageRecord__incoming_video_call_s">Incoming video call · %1$s</string>
|
||||||
|
<!-- Chat log text for a group call that ended that the local user started -->
|
||||||
|
<string name="MessageRecord__outgoing_video_call">Outgoing video call</string>
|
||||||
|
<!-- Chat log text for a group call that ended that the local user started with a placeholder for formatted time -->
|
||||||
|
<string name="MessageRecord__outgoing_video_call_s">Outgoing video call · %1$s</string>
|
||||||
|
<!-- Chat log text for an ongoing group call that the local user started -->
|
||||||
|
<string name="MessageRecord__you_started_a_video_call">You started a video call</string>
|
||||||
|
<!-- Chat log text for an ongoing group call that the local user started with a placeholder for formatted time -->
|
||||||
|
<string name="MessageRecord__you_started_a_video_call_s">You started a video call · %1$s</string>
|
||||||
|
<!-- Chat log text for an ongoing group call that the local user has not joined with a placeholder for user's short display name -->
|
||||||
|
<string name="MessageRecord__s_started_a_video_call">%1$s started a video call</string>
|
||||||
|
<!-- Chat log text for an ongoing group call that the local user has not joined with a placeholder for user's short display name and a placeholder for formatted time -->
|
||||||
|
<string name="MessageRecord__s_started_a_video_call_s">%1$s started a video call · %2$s</string>
|
||||||
|
|
||||||
<string name="MessageRecord_you">You</string>
|
<string name="MessageRecord_you">You</string>
|
||||||
|
|
||||||
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_group_call_s">
|
<!-- Chat log text for an ongoing group call with more than two participants with placeholders for the first two joined users, a placeholder for the number of users in the call, and a placeholder for formatted time -->
|
||||||
|
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_call_s">
|
||||||
<item quantity="one">%1$s, %2$s, and %3$d other are in the group call · %4$s</item>
|
<item quantity="one">%1$s, %2$s, and %3$d other are in the group call · %4$s</item>
|
||||||
<item quantity="other">%1$s, %2$s, and %3$d others are in the group call · %4$s</item>
|
<item quantity="other">%1$s, %2$s, and %3$d others are in the group call · %4$s</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_group_call">
|
<!-- Chat log text for an ongoing group call with more than two participants with placeholders for the first two joined users and a placeholder for the number of users in the call -->
|
||||||
|
<plurals name="MessageRecord_s_s_and_d_others_are_in_the_call">
|
||||||
<item quantity="one">%1$s, %2$s, and %3$d other are in the group call</item>
|
<item quantity="one">%1$s, %2$s, and %3$d other are in the group call</item>
|
||||||
<item quantity="other">%1$s, %2$s, and %3$d others are in the group call</item>
|
<item quantity="other">%1$s, %2$s, and %3$d others are in the group call</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
@ -2712,6 +2729,8 @@
|
||||||
<string name="ConversationUpdateItem_loading">Loading</string>
|
<string name="ConversationUpdateItem_loading">Loading</string>
|
||||||
<string name="ConversationUpdateItem_learn_more">Learn more</string>
|
<string name="ConversationUpdateItem_learn_more">Learn more</string>
|
||||||
<string name="ConversationUpdateItem_join_call">Join call</string>
|
<string name="ConversationUpdateItem_join_call">Join call</string>
|
||||||
|
<!-- Action button label for starting a new call in the current conversation -->
|
||||||
|
<string name="ConversationUpdateItem_call_back">Call back</string>
|
||||||
<string name="ConversationUpdateItem_return_to_call">Return to call</string>
|
<string name="ConversationUpdateItem_return_to_call">Return to call</string>
|
||||||
<string name="ConversationUpdateItem_call_is_full">Call is full</string>
|
<string name="ConversationUpdateItem_call_is_full">Call is full</string>
|
||||||
<string name="ConversationUpdateItem_invite_friends">Invite friends</string>
|
<string name="ConversationUpdateItem_invite_friends">Invite friends</string>
|
||||||
|
|
Loading…
Add table
Reference in a new issue