Compare commits

..

2 Commits

Author SHA1 Message Date
Greyson Parrelli
264a47addf Bump version to 6.32.5 2023-09-14 17:01:33 -04:00
Greyson Parrelli
1c30a077c5 Fix possible crash during JobDatabase upgrade.
This seems to be a SQLite/SQLCipher caching issue.

Fixes #13172
2023-09-14 17:01:05 -04:00
477 changed files with 13761 additions and 17647 deletions

View File

@@ -3,6 +3,7 @@ import com.android.build.api.dsl.ManagedVirtualDevice
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.protobuf'
id 'androidx.navigation.safeargs'
id 'org.jlleitschuh.gradle.ktlint'
id 'org.jetbrains.kotlin.android'
@@ -10,11 +11,25 @@ plugins {
id 'kotlin-parcelize'
id 'com.squareup.wire'
id 'translations'
id 'licenses'
}
apply from: 'static-ips.gradle'
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.18.0'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option "lite"
}
}
}
}
}
wire {
kotlin {
javaInterop = true
@@ -33,15 +48,15 @@ ktlint {
version = "0.49.1"
}
def canonicalVersionCode = 1332
def canonicalVersionName = "6.34.0"
def canonicalVersionCode = 1327
def canonicalVersionName = "6.32.5"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
'armeabi-v7a' : 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4]
def abiPostFix = ['universal' : 5,
'armeabi-v7a' : 6,
'arm64-v8a' : 7,
'x86' : 8,
'x86_64' : 9]
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
@@ -522,8 +537,11 @@ dependencies {
implementation project(':photoview')
implementation libs.libsignal.android
implementation libs.google.protobuf.javalite
implementation libs.mobilecoin
implementation(libs.mobilecoin) {
exclude group: 'com.google.protobuf'
}
implementation libs.signal.ringrtc

View File

@@ -34,7 +34,7 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
SignalExecutors.UNBOUNDED.execute {
Log.blockUntilAllWritesFinished()
LogDatabase.getInstance(this).logs.trimToSize()
LogDatabase.getInstance(this).trimToSize()
}
}
}

View File

@@ -210,7 +210,7 @@ class V2ConversationItemShapeTest {
private val colorizer = Colorizer()
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.Standard
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.STANDARD
override val clickListener: ConversationAdapter.ItemClickListener = FakeConversationItemClickListener
override val selectedItems: Set<MultiselectPart> = emptySet()

View File

@@ -293,22 +293,22 @@ class GroupTableTest {
private fun insertPushGroup(
members: List<DecryptedMember> = listOf(
DecryptedMember.Builder()
.aciBytes(harness.self.requireAci().toByteString())
.joinedAtRevision(0)
.role(Member.Role.DEFAULT)
DecryptedMember.newBuilder()
.setAciBytes(harness.self.requireAci().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build(),
DecryptedMember.Builder()
.aciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString())
.joinedAtRevision(0)
.role(Member.Role.DEFAULT)
DecryptedMember.newBuilder()
.setAciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build()
)
): GroupId {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.Builder()
.members(members)
.revision(0)
val decryptedGroupState = DecryptedGroup.newBuilder()
.addAllMembers(members)
.setRevision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState)!!
@@ -317,23 +317,23 @@ class GroupTableTest {
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val selfMember: DecryptedMember = DecryptedMember.Builder()
.aciBytes(harness.self.requireAci().toByteString())
.joinedAtRevision(0)
.role(Member.Role.DEFAULT)
val selfMember: DecryptedMember = DecryptedMember.newBuilder()
.setAciBytes(harness.self.requireAci().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build()
val otherMembers: List<DecryptedMember> = others.map { id ->
DecryptedMember.Builder()
.aciBytes(Recipient.resolved(id).requireAci().toByteString())
.joinedAtRevision(0)
.role(Member.Role.DEFAULT)
DecryptedMember.newBuilder()
.setAciBytes(Recipient.resolved(id).requireAci().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build()
}
val decryptedGroupState = DecryptedGroup.Builder()
.members(listOf(selfMember) + otherMembers)
.revision(0)
val decryptedGroupState = DecryptedGroup.newBuilder()
.addAllMembers(listOf(selfMember) + otherMembers)
.setRevision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState)!!

View File

@@ -1,288 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import org.junit.Test
import org.signal.core.util.forEach
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.update
import org.thoughtcrime.securesms.crash.CrashConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.testing.assertIs
class LogDatabaseTest {
private val db: LogDatabase = LogDatabase.getInstance(ApplicationDependencies.getApplication())
@Test
fun crashTable_matchesNamePattern() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(namePattern = "Test")
),
promptThreshold = currentTime
)
foundMatch assertIs true
}
@Test
fun crashTable_matchesMessagePattern() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(messagePattern = "Message")
),
promptThreshold = currentTime
)
foundMatch assertIs true
}
@Test
fun crashTable_matchesStackTracePattern() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(stackTracePattern = "stack")
),
promptThreshold = currentTime
)
foundMatch assertIs true
}
@Test
fun crashTable_matchesNameAndMessagePattern() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(namePattern = "Test", messagePattern = "Message")
),
promptThreshold = currentTime
)
foundMatch assertIs true
}
@Test
fun crashTable_matchesNameAndStackTracePattern() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(namePattern = "Test", stackTracePattern = "stack")
),
promptThreshold = currentTime
)
foundMatch assertIs true
}
@Test
fun crashTable_matchesNameAndMessageAndStackTracePattern() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(namePattern = "Test", messagePattern = "Message", stackTracePattern = "stack")
),
promptThreshold = currentTime
)
foundMatch assertIs true
}
@Test
fun crashTable_doesNotMatchNamePattern() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(namePattern = "Blah")
),
promptThreshold = currentTime
)
foundMatch assertIs false
}
@Test
fun crashTable_matchesNameButNotMessagePattern() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(namePattern = "Test", messagePattern = "Blah")
),
promptThreshold = currentTime
)
foundMatch assertIs false
}
@Test
fun crashTable_matchesNameButNotStackTracePattern() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(namePattern = "Test", stackTracePattern = "Blah")
),
promptThreshold = currentTime
)
foundMatch assertIs false
}
@Test
fun crashTable_matchesNamePatternButPromptedTooRecently() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
db.writableDatabase
.update(LogDatabase.CrashTable.TABLE_NAME)
.values(LogDatabase.CrashTable.LAST_PROMPTED_AT to currentTime)
.run()
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(namePattern = "Test")
),
promptThreshold = currentTime - 100
)
foundMatch assertIs false
}
@Test
fun crashTable_noMatches() {
val currentTime = System.currentTimeMillis()
val foundMatch = db.crashes.anyMatch(
listOf(
CrashConfig.CrashPattern(namePattern = "Test")
),
promptThreshold = currentTime - 100
)
foundMatch assertIs false
}
@Test
fun crashTable_updatesLastPromptTime() {
val currentTime = System.currentTimeMillis()
db.crashes.saveCrash(
createdAt = currentTime,
name = "TestName",
message = "Test Message",
stackTrace = "test\nstack\ntrace"
)
db.crashes.saveCrash(
createdAt = currentTime,
name = "XXX",
message = "XXX",
stackTrace = "XXX"
)
db.crashes.markAsPrompted(
listOf(
CrashConfig.CrashPattern(namePattern = "Test")
),
promptedAt = currentTime
)
db.writableDatabase
.select(LogDatabase.CrashTable.NAME, LogDatabase.CrashTable.LAST_PROMPTED_AT)
.from(LogDatabase.CrashTable.TABLE_NAME)
.run()
.forEach {
if (it.requireNonNullString(LogDatabase.CrashTable.NAME) == "TestName") {
it.requireLong(LogDatabase.CrashTable.LAST_PROMPTED_AT) assertIs currentTime
} else {
it.requireLong(LogDatabase.CrashTable.LAST_PROMPTED_AT) assertIs 0
}
}
}
}

View File

@@ -48,7 +48,7 @@ class MessageTableTest_gifts {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
@@ -62,7 +62,7 @@ class MessageTableTest_gifts {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
mms.setOutgoingGiftsRevealed(listOf(messageId))
@@ -76,13 +76,13 @@ class MessageTableTest_gifts {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 2,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
@@ -96,13 +96,13 @@ class MessageTableTest_gifts {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId2 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 2,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
@@ -115,13 +115,13 @@ class MessageTableTest_gifts {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId2 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 2,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
MmsHelper.insert(
@@ -140,13 +140,13 @@ class MessageTableTest_gifts {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId2 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 2,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId3 = MmsHelper.insert(
@@ -165,13 +165,13 @@ class MessageTableTest_gifts {
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 2,
giftBadge = GiftBadge()
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId3 = MmsHelper.insert(

View File

@@ -1228,7 +1228,7 @@ class RecipientTableTest_getAndPossiblyMerge {
.use { cursor: Cursor ->
if (cursor.moveToFirst()) {
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
ThreadMergeEvent.ADAPTER.decode(bytes)
ThreadMergeEvent.parseFrom(bytes)
} else {
null
}
@@ -1246,7 +1246,7 @@ class RecipientTableTest_getAndPossiblyMerge {
.use { cursor: Cursor ->
if (cursor.moveToFirst()) {
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
SessionSwitchoverEvent.ADAPTER.decode(bytes)
SessionSwitchoverEvent.parseFrom(bytes)
} else {
null
}

View File

@@ -22,9 +22,8 @@ import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.MessageTableTestUtils
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.EditMessage
import org.whispersystems.signalservice.internal.push.SyncMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage
import kotlin.time.Duration.Companion.seconds
@RunWith(AndroidJUnit4::class)
@@ -68,17 +67,16 @@ class EditMessageSyncProcessorTest {
val content = MessageContentFuzzer.fuzzTextMessage()
val metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, toRecipient.id)
val syncContent = Content.Builder().syncMessage(
SyncMessage.Builder().sent(
SyncMessage.Sent.Builder()
.destinationServiceId(metadata.destinationServiceId.toString())
.timestamp(originalTimestamp)
.expirationStartTimestamp(originalTimestamp)
.message(content.dataMessage)
.build()
).build()
val syncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
SignalServiceProtos.SyncMessage.newBuilder().setSent(
SignalServiceProtos.SyncMessage.Sent.newBuilder()
.setDestinationServiceId(metadata.destinationServiceId.toString())
.setTimestamp(originalTimestamp)
.setExpirationStartTimestamp(originalTimestamp)
.setMessage(content.dataMessage)
)
).build()
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0)
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer)
val syncTextMessage = TestMessage(
envelope = MessageContentFuzzer.envelope(originalTimestamp),
content = syncContent,
@@ -88,20 +86,18 @@ class EditMessageSyncProcessorTest {
val editTimestamp = originalTimestamp + 200
val editedContent = MessageContentFuzzer.fuzzTextMessage()
val editSyncContent = Content.Builder().syncMessage(
SyncMessage.Builder().sent(
SyncMessage.Sent.Builder()
.destinationServiceId(metadata.destinationServiceId.toString())
.timestamp(editTimestamp)
.expirationStartTimestamp(editTimestamp)
.editMessage(
EditMessage.Builder()
.dataMessage(editedContent.dataMessage)
.targetSentTimestamp(originalTimestamp)
.build()
val editSyncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
SignalServiceProtos.SyncMessage.newBuilder().setSent(
SignalServiceProtos.SyncMessage.Sent.newBuilder()
.setDestinationServiceId(metadata.destinationServiceId.toString())
.setTimestamp(editTimestamp)
.setExpirationStartTimestamp(editTimestamp)
.setEditMessage(
EditMessage.newBuilder()
.setDataMessage(editedContent.dataMessage)
.setTargetSentTimestamp(originalTimestamp)
)
.build()
).build()
)
).build()
val syncEditMessage = TestMessage(
@@ -113,38 +109,38 @@ class EditMessageSyncProcessorTest {
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000)
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer / 1000)
val originalTextMessage = OutgoingMessage(
threadRecipient = toRecipient,
sentTimeMillis = originalTimestamp,
body = content.dataMessage?.body ?: "",
expiresIn = content.dataMessage?.expireTimer?.seconds?.inWholeMilliseconds ?: 0,
body = content.dataMessage.body,
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
isUrgent = true,
isSecure = true,
bodyRanges = content.dataMessage?.bodyRanges.toBodyRangeList()
bodyRanges = content.dataMessage.bodyRangesList.toBodyRangeList()
)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient)
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(originalMessageId, true)
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
if (content.dataMessage.expireTimer > 0) {
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
}
val editMessage = OutgoingMessage(
threadRecipient = toRecipient,
sentTimeMillis = editTimestamp,
body = editedContent.dataMessage?.body ?: "",
expiresIn = content.dataMessage?.expireTimer?.seconds?.inWholeMilliseconds ?: 0,
body = editedContent.dataMessage.body,
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
isUrgent = true,
isSecure = true,
bodyRanges = editedContent.dataMessage?.bodyRanges.toBodyRangeList(),
bodyRanges = editedContent.dataMessage.bodyRangesList.toBodyRangeList(),
messageToEdit = originalMessageId
)
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(editMessageId, true)
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
if (content.dataMessage.expireTimer > 0) {
SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp)
}
testResult.collectLocal()
@@ -171,7 +167,7 @@ class EditMessageSyncProcessorTest {
fun runSync(messages: List<TestMessage>) {
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
if (content.syncMessage != null) {
if (content.hasSyncMessage()) {
processorV2.process(
envelope,
content,

View File

@@ -1,13 +1,13 @@
package org.thoughtcrime.securesms.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import okio.ByteString.Companion.toByteString
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
@@ -15,8 +15,8 @@ import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.MessageTableTestUtils
import org.whispersystems.signalservice.internal.push.DataMessage
import org.whispersystems.signalservice.internal.push.GroupContextV2
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
@@ -41,9 +41,9 @@ class MessageContentProcessor__recipientStatusTest {
@Test
fun syncGroupSentTextMessageWithRecipientUpdateFollowup() {
val (groupId, masterKey, groupRecipientId) = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), harness.others[0].asMember(), harness.others[1].asMember())
val groupContextV2 = GroupContextV2.Builder().revision(0).masterKey(masterKey.serialize().toByteString()).build()
val groupContextV2 = GroupContextV2.newBuilder().setRevision(0).setMasterKey(masterKey.serialize().toProtoByteString()).build()
val initialTextMessage = DataMessage.Builder().buildWith {
val initialTextMessage = DataMessage.newBuilder().buildWith {
body = MessageContentFuzzer.string()
groupV2 = groupContextV2
timestamp = envelopeTimestamp

View File

@@ -6,6 +6,7 @@ import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.junit.After
import org.junit.Before
import org.junit.Ignore
@@ -25,7 +26,7 @@ import org.thoughtcrime.securesms.testing.Entry
import org.thoughtcrime.securesms.testing.FakeClientHelpers
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.awaitFor
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
import org.whispersystems.signalservice.internal.websocket.WebSocketMessage
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import java.util.regex.Pattern
@@ -92,7 +93,7 @@ class MessageProcessingPerformanceTest {
val messageCount = 100
val envelopes = generateInboundEnvelopes(bobClient, messageCount)
val firstTimestamp = envelopes.first().timestamp
val lastTimestamp = envelopes.last().timestamp ?: 0
val lastTimestamp = envelopes.last().timestamp
// Inject the envelopes into the websocket
Thread {
@@ -189,7 +190,7 @@ class MessageProcessingPerformanceTest {
path = "/api/v1/message",
id = Random(System.currentTimeMillis()).nextLong(),
headers = listOf("X-Signal-Timestamp: ${this.timestamp}"),
body = this.encodeByteString()
body = this.toByteArray().toByteString()
)
).encodeByteString()
}

View File

@@ -1,12 +1,11 @@
package org.thoughtcrime.securesms.messages
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
data class TestMessage(
val envelope: Envelope,
val content: Content,
val envelope: SignalServiceProtos.Envelope,
val content: SignalServiceProtos.Content,
val metadata: EnvelopeMetadata,
val serverDeliveredTimestamp: Long
)

View File

@@ -5,8 +5,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.testing.LogPredicate
import org.thoughtcrime.securesms.util.SignalLocalMetrics
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(context) {
companion object {
@@ -20,9 +19,9 @@ class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(
fun endTag(timestamp: Long) = "$timestamp end"
}
override fun process(envelope: Envelope, content: Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean, localMetric: SignalLocalMetrics.MessageReceive?) {
Log.d(TAG, startTag(envelope.timestamp!!))
override fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean, localMetric: SignalLocalMetrics.MessageReceive?) {
Log.d(TAG, startTag(envelope.timestamp))
super.process(envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent, localMetric)
Log.d(TAG, endTag(envelope.timestamp!!))
Log.d(TAG, endTag(envelope.timestamp))
}
}

View File

@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
/**
* Welcome to Alice's Client.

View File

@@ -31,10 +31,11 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import java.util.Optional
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
import kotlin.UnsupportedOperationException
/**
* Welcome to Bob's Client.
@@ -60,7 +61,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
}
/** Inspired by SignalServiceMessageSender#getEncryptedMessage */
fun encrypt(now: Long): Envelope {
fun encrypt(now: Long): SignalServiceProtos.Envelope {
val envelopeContent = FakeClientHelpers.encryptedTextMessage(now)
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null)
@@ -71,10 +72,10 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
}
return cipher.encrypt(getAliceProtocolAddress(), getAliceUnidentifiedAccess(), envelopeContent)
.toEnvelope(envelopeContent.content.get().dataMessage!!.timestamp!!, getAliceServiceId())
.toEnvelope(envelopeContent.content.get().dataMessage.timestamp, getAliceServiceId())
}
fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) {
fun decrypt(envelope: SignalServiceProtos.Envelope, serverDeliveredTimestamp: Long) {
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator())
cipher.decrypt(envelope, serverDeliveredTimestamp)
}

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.testing
import okio.ByteString.Companion.toByteString
import org.signal.libsignal.internal.Native
import org.signal.libsignal.internal.NativeHandleGuard
import org.signal.libsignal.metadata.certificate.CertificateValidator
@@ -10,16 +9,15 @@ import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
import org.whispersystems.util.Base64
import java.util.Optional
import java.util.UUID
@@ -54,9 +52,9 @@ object FakeClientHelpers {
}
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {
val content = Content.Builder().apply {
dataMessage(
DataMessage.Builder().buildWith {
val content = SignalServiceProtos.Content.newBuilder().apply {
setDataMessage(
SignalServiceProtos.DataMessage.newBuilder().apply {
body = message
timestamp = now
}
@@ -66,16 +64,16 @@ object FakeClientHelpers {
}
fun OutgoingPushMessage.toEnvelope(timestamp: Long, destination: ServiceId): Envelope {
return Envelope.Builder()
.type(Envelope.Type.fromValue(this.type))
.sourceDevice(1)
.timestamp(timestamp)
.serverTimestamp(timestamp + 1)
.destinationServiceId(destination.toString())
.serverGuid(UUID.randomUUID().toString())
.content(Base64.decode(this.content).toByteString())
.urgent(true)
.story(false)
return Envelope.newBuilder()
.setType(Envelope.Type.valueOf(this.type))
.setSourceDevice(1)
.setTimestamp(timestamp)
.setServerTimestamp(timestamp + 1)
.setDestinationServiceId(destination.toString())
.setServerGuid(UUID.randomUUID().toString())
.setContent(Base64.decode(this.content).toProtoByteString())
.setUrgent(true)
.setStory(false)
.build()
}
}

View File

@@ -16,19 +16,19 @@ import kotlin.random.Random
*/
object GroupTestingUtils {
fun member(aci: ACI, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
return DecryptedMember.Builder()
.aciBytes(aci.toByteString())
.joinedAtRevision(revision)
.role(role)
return DecryptedMember.newBuilder()
.setAciBytes(aci.toByteString())
.setJoinedAtRevision(revision)
.setRole(role)
.build()
}
fun insertGroup(revision: Int = 0, vararg members: DecryptedMember): TestGroupInfo {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.Builder()
.members(members.toList())
.revision(revision)
.title(MessageContentFuzzer.string())
val decryptedGroupState = DecryptedGroup.newBuilder()
.addAllMembers(members.toList())
.setRevision(revision)
.setTitle(MessageContentFuzzer.string())
.build()
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!!

View File

@@ -1,20 +1,21 @@
package org.thoughtcrime.securesms.testing
import okio.ByteString
import okio.ByteString.Companion.toByteString
import com.google.protobuf.ByteString
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.thoughtcrime.securesms.messages.TestMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.AttachmentPointer
import org.whispersystems.signalservice.internal.push.BodyRange
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.GroupContextV2
import org.whispersystems.signalservice.internal.push.SyncMessage
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
import java.util.UUID
import kotlin.random.Random
import kotlin.random.nextInt
@@ -34,10 +35,10 @@ object MessageContentFuzzer {
* Create an [Envelope].
*/
fun envelope(timestamp: Long): Envelope {
return Envelope.Builder()
.timestamp(timestamp)
.serverTimestamp(timestamp + 5)
.serverGuid(UUID.randomUUID().toString())
return Envelope.newBuilder()
.setTimestamp(timestamp)
.setServerTimestamp(timestamp + 5)
.setServerGuidBytes(UuidUtil.toByteString(UUID.randomUUID()))
.build()
}
@@ -61,22 +62,20 @@ object MessageContentFuzzer {
* - Bold style body ranges
*/
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
return Content.newBuilder()
.setDataMessage(
DataMessage.newBuilder().buildWith {
body = string()
if (random.nextBoolean()) {
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
}
if (random.nextBoolean()) {
bodyRanges(
listOf(
BodyRange.Builder().buildWith {
start = 0
length = 1
style = BodyRange.Style.BOLD
}
)
addBodyRanges(
SignalServiceProtos.BodyRange.newBuilder().buildWith {
start = 0
length = 1
style = SignalServiceProtos.BodyRange.Style.BOLD
}
)
}
if (groupContextV2 != null) {
@@ -96,16 +95,16 @@ object MessageContentFuzzer {
recipientUpdate: Boolean = false
): Content {
return Content
.Builder()
.syncMessage(
SyncMessage.Builder().buildWith {
sent = SyncMessage.Sent.Builder().buildWith {
.newBuilder()
.setSyncMessage(
SyncMessage.newBuilder().buildWith {
sent = SyncMessage.Sent.newBuilder().buildWith {
timestamp = textMessage.timestamp
message = textMessage
isRecipientUpdate = recipientUpdate
unidentifiedStatus(
addAllUnidentifiedStatus(
deliveredTo.map {
SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder().buildWith {
SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder().buildWith {
destinationServiceId = Recipient.resolved(it).requireServiceId().toString()
unidentified = true
}
@@ -124,9 +123,9 @@ object MessageContentFuzzer {
* - A message with 0-2 attachment pointers and may contain a text body
*/
fun fuzzMediaMessageWithBody(quoteAble: List<TestMessage> = emptyList()): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
return Content.newBuilder()
.setDataMessage(
DataMessage.newBuilder().buildWith {
if (random.nextBoolean()) {
body = string()
}
@@ -134,28 +133,28 @@ object MessageContentFuzzer {
if (random.nextBoolean() && quoteAble.isNotEmpty()) {
body = string()
val quoted = quoteAble.random(random)
quote = DataMessage.Quote.Builder().buildWith {
quote = DataMessage.Quote.newBuilder().buildWith {
id = quoted.envelope.timestamp
authorAci = quoted.metadata.sourceServiceId.toString()
text = quoted.content.dataMessage?.body
attachments(quoted.content.dataMessage?.attachments ?: emptyList())
bodyRanges(quoted.content.dataMessage?.bodyRanges ?: emptyList())
text = quoted.content.dataMessage.body
addAllAttachments(quoted.content.dataMessage.attachmentsList)
addAllBodyRanges(quoted.content.dataMessage.bodyRangesList)
type = DataMessage.Quote.Type.NORMAL
}
}
if (random.nextFloat() < 0.1 && quoteAble.isNotEmpty()) {
val quoted = quoteAble.random(random)
quote = DataMessage.Quote.Builder().buildWith {
id = random.nextLong(quoted.envelope.timestamp!! - 1000000, quoted.envelope.timestamp!!)
quote = DataMessage.Quote.newBuilder().buildWith {
id = random.nextLong(quoted.envelope.timestamp - 1000000, quoted.envelope.timestamp)
authorAci = quoted.metadata.sourceServiceId.toString()
text = quoted.content.dataMessage?.body
text = quoted.content.dataMessage.body
}
}
if (random.nextFloat() < 0.25) {
val total = random.nextInt(1, 2)
attachments((0..total).map { attachmentPointer() })
(0..total).forEach { _ -> addAttachments(attachmentPointer()) }
}
}
)
@@ -167,12 +166,12 @@ object MessageContentFuzzer {
* - A reaction to a prior message
*/
fun fuzzMediaMessageNoContent(previousMessages: List<TestMessage> = emptyList()): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
return Content.newBuilder()
.setDataMessage(
DataMessage.newBuilder().buildWith {
if (random.nextFloat() < 0.25) {
val reactTo = previousMessages.random(random)
reaction = DataMessage.Reaction.Builder().buildWith {
reaction = DataMessage.Reaction.newBuilder().buildWith {
emoji = emojis.random(random)
remove = false
targetAuthorAci = reactTo.metadata.sourceServiceId.toString()
@@ -188,15 +187,15 @@ object MessageContentFuzzer {
* - A sticker
*/
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
return Content.newBuilder()
.setDataMessage(
DataMessage.newBuilder().buildWith {
if (random.nextFloat() < 0.9) {
sticker = DataMessage.Sticker.Builder().buildWith {
sticker = DataMessage.Sticker.newBuilder().buildWith {
packId = byteString(length = 24)
packKey = byteString(length = 128)
stickerId = random.nextInt()
data_ = attachmentPointer()
data = attachmentPointer()
emoji = emojis.random(random)
}
}
@@ -224,14 +223,14 @@ object MessageContentFuzzer {
* Generate a random [ByteString].
*/
fun byteString(length: Int = 512): ByteString {
return random.nextBytes(length).toByteString()
return random.nextBytes(length).toProtoByteString()
}
/**
* Generate a random [AttachmentPointer].
*/
fun attachmentPointer(): AttachmentPointer {
return AttachmentPointer.Builder().run {
return AttachmentPointer.newBuilder().run {
cdnKey = string()
contentType = mediaTypes.random(random)
key = byteString()

View File

@@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.testing
import com.google.protobuf.ByteString
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
import org.whispersystems.signalservice.internal.serialize.protos.AddressProto
import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
import java.util.UUID
import kotlin.random.Random
class TestProtos private constructor() {
fun address(
uuid: UUID = UUID.randomUUID()
): AddressProto.Builder {
return AddressProto.newBuilder()
.setUuid(ACI.from(uuid).toByteString())
}
fun metadata(
address: AddressProto = address().build()
): MetadataProto.Builder {
return MetadataProto.newBuilder()
.setAddress(address)
}
fun groupContextV2(
revision: Int = 0,
masterKeyBytes: ByteArray = Random.Default.nextBytes(GroupMasterKey.SIZE)
): GroupContextV2.Builder {
return GroupContextV2.newBuilder()
.setRevision(revision)
.setMasterKey(ByteString.copyFrom(masterKeyBytes))
}
fun storyContext(
sentTimestamp: Long = Random.nextLong(),
authorUuid: String = UUID.randomUUID().toString()
): DataMessage.StoryContext.Builder {
return DataMessage.StoryContext.newBuilder()
.setAuthorAci(authorUuid)
.setSentTimestamp(sentTimestamp)
}
fun dataMessage(): DataMessage.Builder {
return DataMessage.newBuilder()
}
fun content(): SignalServiceProtos.Content.Builder {
return SignalServiceProtos.Content.newBuilder()
}
fun serviceContent(
localAddress: AddressProto = address().build(),
metadata: MetadataProto = metadata().build()
): SignalServiceContentProto.Builder {
return SignalServiceContentProto.newBuilder()
.setLocalAddress(localAddress)
.setMetadata(metadata)
}
companion object {
fun <T> build(buildFn: TestProtos.() -> T): T {
return TestProtos().buildFn()
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -302,8 +302,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
SignalExecutors.UNBOUNDED.execute(() -> {
Log.blockUntilAllWritesFinished();
LogDatabase.getInstance(this).logs().trimToSize();
LogDatabase.getInstance(this).crashes().trimToSize();
LogDatabase.getInstance(this).trimToSize();
});
}

View File

@@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSh
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
import org.thoughtcrime.securesms.notifications.VitalsViewModel;
import org.thoughtcrime.securesms.notifications.SlowNotificationsViewModel;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
import org.thoughtcrime.securesms.util.AppStartup;
@@ -45,7 +45,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
private VoiceNoteMediaController mediaController;
private ConversationListTabsViewModel conversationListTabsViewModel;
private VitalsViewModel vitalsViewModel;
private SlowNotificationsViewModel slowNotificationsViewModel;
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
@@ -99,27 +99,25 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
updateTabVisibility();
vitalsViewModel = new ViewModelProvider(this).get(VitalsViewModel.class);
slowNotificationsViewModel = new ViewModelProvider(this).get(SlowNotificationsViewModel.class);
lifecycleDisposable.add(
vitalsViewModel
.getVitalsState()
.subscribe(this::presentVitalsState)
slowNotificationsViewModel
.getSlowNotificationState()
.subscribe(this::presentSlowNotificationState)
);
}
@SuppressLint("NewApi")
private void presentVitalsState(VitalsViewModel.State state) {
switch (state) {
private void presentSlowNotificationState(SlowNotificationsViewModel.State slowNotificationState) {
switch (slowNotificationState) {
case NONE:
break;
case PROMPT_BATTERY_SAVER_DIALOG:
PromptBatterySaverDialogFragment.show(getSupportFragmentManager());
break;
case PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS:
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager(), DebugLogsPromptDialogFragment.Purpose.NOTIFICATIONS);
case PROMPT_DEBUGLOGS_FOR_CRASH:
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager(), DebugLogsPromptDialogFragment.Purpose.CRASH);
case PROMPT_DEBUGLOGS:
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager());
break;
}
}
@@ -170,7 +168,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
updateTabVisibility();
vitalsViewModel.checkSlowNotificationHeuristics();
slowNotificationsViewModel.checkSlowNotificationHeuristics();
}
@Override

View File

@@ -73,7 +73,6 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls;
import org.thoughtcrime.securesms.components.webrtc.WifiToCellularPopupWindow;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -1089,11 +1088,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public void onLaunchPendingRequestsSheet() {
new PendingParticipantsBottomSheet().show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
@Override
public void onLaunchRecipientSheet(@NonNull Recipient pendingRecipient) {
CallLinkIncomingRequestSheet.show(getSupportFragmentManager(), pendingRecipient.getId());
}
}
private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> {

View File

@@ -1,10 +1,10 @@
package org.thoughtcrime.securesms.absbackup.backupables
import com.google.protobuf.InvalidProtocolBufferException
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.absbackup.AndroidBackupItem
import org.thoughtcrime.securesms.absbackup.protos.SvrAuthToken
import org.thoughtcrime.securesms.keyvalue.SignalStore
import java.io.IOException
/**
* This backs up the not-secret KBS Auth tokens, which can be combined with a PIN to prove ownership of a phone number in order to complete the registration process.
@@ -30,7 +30,7 @@ object SvrAuthTokens : AndroidBackupItem {
val proto = SvrAuthToken.ADAPTER.decode(data)
SignalStore.svr().putAuthTokenList(proto.tokens)
} catch (e: IOException) {
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, "Cannot restore KbsAuthToken from backup service.")
}
}

View File

@@ -172,11 +172,7 @@ public abstract class Attachment {
@Nullable
public byte[] getIncrementalDigest() {
if (incrementalDigest != null && incrementalDigest.length > 0) {
return incrementalDigest;
} else {
return null;
}
return incrementalDigest;
}
@Nullable

View File

@@ -13,7 +13,7 @@ import org.whispersystems.signalservice.api.InvalidMessageStructureException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil;
import org.whispersystems.signalservice.internal.push.DataMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.util.LinkedList;
import java.util.List;
@@ -152,18 +152,18 @@ public class PointerAttachment extends Attachment {
null));
}
public static Optional<Attachment> forPointer(DataMessage.Quote.QuotedAttachment quotedAttachment) {
public static Optional<Attachment> forPointer(SignalServiceProtos.DataMessage.Quote.QuotedAttachment quotedAttachment) {
SignalServiceAttachment thumbnail;
try {
thumbnail = quotedAttachment.thumbnail != null ? AttachmentPointerUtil.createSignalAttachmentPointer(quotedAttachment.thumbnail) : null;
thumbnail = quotedAttachment.hasThumbnail() ? AttachmentPointerUtil.createSignalAttachmentPointer(quotedAttachment.getThumbnail()) : null;
} catch (InvalidMessageStructureException e) {
return Optional.empty();
}
return Optional.of(new PointerAttachment(quotedAttachment.contentType,
return Optional.of(new PointerAttachment(quotedAttachment.getContentType(),
AttachmentTable.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
quotedAttachment.fileName,
quotedAttachment.getFileName(),
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,

View File

@@ -2,19 +2,19 @@ package org.thoughtcrime.securesms.audio;
import androidx.annotation.NonNull;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
import java.util.concurrent.TimeUnit;
import okio.ByteString;
public class AudioFileInfo {
private final long durationUs;
private final byte[] waveFormBytes;
private final float[] waveForm;
public static @NonNull AudioFileInfo fromDatabaseProtobuf(@NonNull AudioWaveFormData audioWaveForm) {
return new AudioFileInfo(audioWaveForm.durationUs, audioWaveForm.waveForm.toByteArray());
return new AudioFileInfo(audioWaveForm.getDurationUs(), audioWaveForm.getWaveForm().toByteArray());
}
AudioFileInfo(long durationUs, byte[] waveFormBytes) {
@@ -37,9 +37,9 @@ public class AudioFileInfo {
}
public @NonNull AudioWaveFormData toDatabaseProtobuf() {
return new AudioWaveFormData.Builder()
.durationUs(durationUs)
.waveForm(ByteString.of(waveFormBytes))
.build();
return AudioWaveFormData.newBuilder()
.setDurationUs(durationUs)
.setWaveForm(ByteString.copyFrom(waveFormBytes))
.build();
}
}

View File

@@ -22,13 +22,13 @@ public final class AudioHash {
}
public AudioHash(@NonNull AudioWaveFormData audioWaveForm) {
this(Base64.encodeBytes(audioWaveForm.encode()), audioWaveForm);
this(Base64.encodeBytes(audioWaveForm.toByteArray()), audioWaveForm);
}
public static @Nullable AudioHash parseOrNull(@Nullable String hash) {
if (hash == null) return null;
try {
return new AudioHash(hash, AudioWaveFormData.ADAPTER.decode(Base64.decode(hash)));
return new AudioHash(hash, AudioWaveFormData.parseFrom(Base64.decode(hash)));
} catch (IOException e) {
return null;
}

View File

@@ -107,7 +107,7 @@ object AudioWaveForms {
private fun generateWaveForm(context: Context, uri: Uri, cacheKey: String, attachmentId: AttachmentId): CacheCheckResult {
try {
val startTime = System.currentTimeMillis()
SignalDatabase.attachments.writeAudioHash(attachmentId, AudioWaveFormData())
SignalDatabase.attachments.writeAudioHash(attachmentId, AudioWaveFormData.getDefaultInstance())
Log.i(TAG, "Starting wave form generation ($cacheKey)")
val fileInfo: AudioFileInfo = AudioWaveFormGenerator.generateWaveForm(context, uri)

View File

@@ -101,16 +101,16 @@ object Badges {
@JvmStatic
fun toDatabaseBadge(badge: Badge): BadgeList.Badge {
return BadgeList.Badge(
id = badge.id,
category = badge.category.code,
description = badge.description,
expiration = badge.expirationTimestamp,
visible = badge.visible,
name = badge.name,
imageUrl = badge.imageUrl.toString(),
imageDensity = badge.imageDensity
)
return BadgeList.Badge.newBuilder()
.setId(badge.id)
.setCategory(badge.category.code)
.setDescription(badge.description)
.setExpiration(badge.expirationTimestamp)
.setVisible(badge.visible)
.setName(badge.name)
.setImageUrl(badge.imageUrl.toString())
.setImageDensity(badge.imageDensity)
.build()
}
@JvmStatic

View File

@@ -79,11 +79,12 @@ class GiftMessageView @JvmOverloads constructor(
}
actionView.setText(
when (giftBadge.redemptionState) {
when (giftBadge.redemptionState ?: GiftBadge.RedemptionState.UNRECOGNIZED) {
GiftBadge.RedemptionState.PENDING -> R.string.GiftMessageView__redeem
GiftBadge.RedemptionState.STARTED -> R.string.GiftMessageView__redeeming
GiftBadge.RedemptionState.REDEEMED -> R.string.GiftMessageView__redeemed
GiftBadge.RedemptionState.FAILED -> R.string.GiftMessageView__redeem
GiftBadge.RedemptionState.UNRECOGNIZED -> R.string.GiftMessageView__redeem
}
)
}

View File

@@ -32,7 +32,7 @@ object Gifts {
): OutgoingMessage {
return OutgoingMessage(
threadRecipient = recipient,
body = Base64.encodeBytes(giftBadge.encode()),
body = Base64.encodeBytes(giftBadge.toByteArray()),
isSecure = true,
sentTimeMillis = sentTimestamp,
expiresIn = expiresIn,

View File

@@ -63,7 +63,7 @@ class ViewReceivedGiftBottomSheet : DSLSettingsBottomSheetFragment() {
ViewReceivedGiftBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(ARG_SENT_FROM, messageRecord.fromRecipient.id)
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.encode())
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.toByteArray())
putLong(ARG_MESSAGE_ID, messageRecord.id)
}
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)

View File

@@ -34,7 +34,7 @@ class ViewSentGiftBottomSheet : DSLSettingsBottomSheetFragment() {
ViewSentGiftBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(ARG_SENT_TO, messageRecord.toRecipient.id)
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.encode())
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.toByteArray())
}
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
@@ -45,7 +45,7 @@ class ViewSentGiftBottomSheet : DSLSettingsBottomSheetFragment() {
get() = requireArguments().getParcelableCompat(ARG_SENT_TO, RecipientId::class.java)!!
private val giftBadge: GiftBadge
get() = GiftBadge.ADAPTER.decode(requireArguments().getByteArray(ARG_GIFT_BADGE)!!)
get() = GiftBadge.parseFrom(requireArguments().getByteArray(ARG_GIFT_BADGE))
private val lifecycleDisposable = LifecycleDisposable()

View File

@@ -7,16 +7,12 @@ package org.thoughtcrime.securesms.calls.links
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorRes
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton
import org.thoughtcrime.securesms.R
/**
* ConversationItem action button for joining a call link.
*/
class CallLinkJoinButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
@@ -26,19 +22,10 @@ class CallLinkJoinButton @JvmOverloads constructor(
inflate(context, R.layout.call_link_join_button, this)
}
private val joinStroke: View = findViewById(R.id.join_stroke)
private val joinButton: MaterialButton = findViewById(R.id.join_button)
fun setTextColor(@ColorRes textColorResId: Int) {
val color = ContextCompat.getColor(context, textColorResId)
joinButton.setTextColor(color)
}
fun setStrokeColor(@ColorRes strokeColorResId: Int) {
val color = ContextCompat.getColor(context, strokeColorResId)
joinStroke.setBackgroundColor(color)
joinButton.setTextColor(ContextCompat.getColor(context, textColorResId))
}
fun setJoinClickListener(onClickListener: OnClickListener) {

View File

@@ -59,6 +59,7 @@ object CallLinks {
}
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
Log.w(TAG, "Invalid url prefix.")
return false
}

View File

@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -133,10 +132,6 @@ fun SignalCallRow(
Buttons.Small(
onClick = onJoinClicked,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.align(CenterVertically)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))

View File

@@ -106,13 +106,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
Spacer(modifier = Modifier.height(12.dp))
Rows.TextRow(
text = stringResource(
id = if (callLink.state.name.isEmpty()) {
R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name
} else {
R.string.CreateCallLinkBottomSheetDialogFragment__edit_call_name
}
),
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name),
onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked
)

View File

@@ -246,13 +246,7 @@ private fun CallLinkDetails(
if (state.callLink.credentials?.adminPassBytes != null) {
Rows.TextRow(
text = stringResource(
id = if (state.callLink.state.name.isEmpty()) {
R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name
} else {
R.string.CreateCallLinkBottomSheetDialogFragment__edit_call_name
}
),
text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name),
onClick = callback::onEditNameClicked
)

View File

@@ -57,7 +57,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.tabs.ConversationListTab
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.doAfterNextLayout
import org.thoughtcrime.securesms.util.fragments.requireListener
@@ -231,13 +230,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
val count = callLogActionMode.getCount()
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, count, count))
.setMessage(
if (FeatureFlags.adHocCalling()) {
getString(R.string.CallLogFragment__call_links_youve_created)
} else {
null
}
)
.setPositiveButton(R.string.CallLogFragment__delete) { _, _ ->
performDeletion(count, viewModel.stageSelectionDeletion())
callLogActionMode.end()
@@ -371,13 +363,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
override fun deleteCall(call: CallLogRow) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, 1, 1))
.setMessage(
if (FeatureFlags.adHocCalling()) {
getString(R.string.CallLogFragment__call_links_youve_created)
} else {
null
}
)
.setPositiveButton(R.string.CallLogFragment__delete) { _, _ ->
performDeletion(1, viewModel.stageCallDeletion(call))
}

View File

@@ -63,7 +63,7 @@ class CallLogPagedDataSource(
remaining -= callEvents.size
}
if (hasFilter && start <= clearFilterStart && remaining > 0) {
if (start <= clearFilterStart && remaining > 0) {
callLogRows.add(CallLogRow.ClearFilter)
}

View File

@@ -93,11 +93,11 @@ sealed class CallLogRow {
return FULL
}
if (groupCallUpdateDetails.inCallUuids.contains(Recipient.self().requireAci().rawUuid.toString())) {
if (groupCallUpdateDetails.inCallUuidsList.contains(Recipient.self().requireAci().rawUuid.toString())) {
return LOCAL_USER_JOINED
}
return if (groupCallUpdateDetails.inCallUuids.isNotEmpty()) {
return if (groupCallUpdateDetails.inCallUuidsCount > 0) {
ACTIVE
} else {
NONE

View File

@@ -12,7 +12,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
@@ -55,7 +54,7 @@ public class AlbumThumbnailView extends FrameLayout {
inflate(getContext(), R.layout.album_thumbnail_view, this);
albumCellContainer = findViewById(R.id.album_cell_container);
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
transferControls = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
}
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) {
@@ -65,13 +64,12 @@ public class AlbumThumbnailView extends FrameLayout {
if (showControls) {
transferControls.get().setShowDownloadText(true);
transferControls.get().setSlides(slides);
transferControls.get().setDownloadClickListener(v -> {
if (downloadClickListener != null) {
downloadClickListener.onClick(v, slides);
}
});
transferControls.get().setSlides(slides);
transferControls.setVisibility(VISIBLE);
} else {
if (transferControls.resolved()) {
transferControls.get().setVisibility(GONE);
@@ -87,7 +85,6 @@ public class AlbumThumbnailView extends FrameLayout {
showSlides(glideRequests, slides);
applyCorners();
forceLayout();
}
public void setCellBackgroundColor(@ColorInt int color) {
@@ -120,25 +117,22 @@ public class AlbumThumbnailView extends FrameLayout {
private void inflateLayout(int sizeClass) {
albumCellContainer.removeAllViews();
int resId = switch (sizeClass) {
case 2 -> R.layout.album_thumbnail_2;
case 3 -> R.layout.album_thumbnail_3;
case 4 -> R.layout.album_thumbnail_4;
case 5 -> R.layout.album_thumbnail_5;
default -> R.layout.album_thumbnail_many;
};
inflate(getContext(), resId, albumCellContainer);
if (transferControls.resolved()) {
int size = switch (sizeClass) {
case 2 -> R.dimen.album_2_total_height;
case 3 -> R.dimen.album_3_total_height;
case 4 -> R.dimen.album_4_total_height;
default -> R.dimen.album_5_total_height;
};
final ViewGroup.LayoutParams params = transferControls.get().getLayoutParams();
params.height = getContext().getResources().getDimensionPixelSize(size);
transferControls.get().setLayoutParams(params);
switch (sizeClass) {
case 2:
inflate(getContext(), R.layout.album_thumbnail_2, albumCellContainer);
break;
case 3:
inflate(getContext(), R.layout.album_thumbnail_3, albumCellContainer);
break;
case 4:
inflate(getContext(), R.layout.album_thumbnail_4, albumCellContainer);
break;
case 5:
inflate(getContext(), R.layout.album_thumbnail_5, albumCellContainer);
break;
default:
inflate(getContext(), R.layout.album_thumbnail_many, albumCellContainer);
break;
}
}

View File

@@ -322,12 +322,12 @@ public class ConversationItemFooter extends ConstraintLayout {
} else {
long timestamp = messageRecord.getTimestamp();
if (messageRecord.isEditMessage()) {
if (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
if (displayMode == ConversationItemDisplayMode.EDIT_HISTORY) {
timestamp = messageRecord.getDateSent();
}
}
String date = DateUtils.getDatelessRelativeTimeSpanString(getContext(), locale, timestamp);
if (displayMode != ConversationItemDisplayMode.Detailed.INSTANCE && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) {
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) {
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date);
}
dateView.setText(date);

View File

@@ -255,17 +255,9 @@ class ConversationItemThumbnail @JvmOverloads constructor(
state.applyState(thumbnail, album)
}
fun setPlayVideoClickListener(listener: SlideClickListener?) {
fun setProgressWheelClickListener(listener: SlideClickListener?) {
state = state.copy(
thumbnailViewState = state.thumbnailViewState.copy(playVideoClickListener = listener)
)
state.applyState(thumbnail, album)
}
fun setCancelDownloadClickListener(listener: SlidesClickedListener?) {
state = state.copy(
thumbnailViewState = state.thumbnailViewState.copy(cancelDownloadClickListener = listener)
thumbnailViewState = state.thumbnailViewState.copy(progressWheelClickListener = listener)
)
state.applyState(thumbnail, album)

View File

@@ -31,9 +31,7 @@ data class ConversationItemThumbnailState(
@IgnoredOnParcel
private val downloadClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel
private val cancelDownloadClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel
private val playVideoClickListener: SlideClickListener? = null,
private val progressWheelClickListener: SlideClickListener? = null,
@IgnoredOnParcel
private val longClickListener: OnLongClickListener? = null,
private val visibility: Int = View.GONE,
@@ -59,8 +57,7 @@ data class ConversationItemThumbnailState(
thumbnailView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
thumbnailView.get().setThumbnailClickListener(clickListener)
thumbnailView.get().setDownloadClickListener(downloadClickListener)
thumbnailView.get().setCancelDownloadClickListener(cancelDownloadClickListener)
thumbnailView.get().setPlayVideoClickListener(playVideoClickListener)
thumbnailView.get().setProgressWheelClickListener(progressWheelClickListener)
thumbnailView.get().setOnLongClickListener(longClickListener)
thumbnailView.get().setBounds(minWidth, maxWidth, minHeight, maxHeight)
}

View File

@@ -13,12 +13,11 @@ import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import org.signal.core.util.ResourceUtil
import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.PromptLogsBottomSheetBinding
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
@@ -28,21 +27,14 @@ import org.thoughtcrime.securesms.util.SupportEmailUtil
class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
companion object {
private const val KEY_PURPOSE = "purpose"
@JvmStatic
fun show(context: Context, fragmentManager: FragmentManager, purpose: Purpose) {
fun show(context: Context, fragmentManager: FragmentManager) {
if (NetworkUtil.isConnected(context) && fragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) {
DebugLogsPromptDialogFragment().apply {
arguments = bundleOf(
KEY_PURPOSE to purpose.serialized
)
arguments = bundleOf()
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
when (purpose) {
Purpose.NOTIFICATIONS -> SignalStore.uiHints().lastNotificationLogsPrompt = System.currentTimeMillis()
Purpose.CRASH -> SignalStore.uiHints().lastCrashPrompt = System.currentTimeMillis()
}
SignalStore.uiHints().lastNotificationLogsPrompt = System.currentTimeMillis()
}
}
}
@@ -52,12 +44,7 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
private val binding by ViewBinderDelegate(PromptLogsBottomSheetBinding::bind)
private val viewModel: PromptLogsViewModel by viewModels(
factoryProducer = {
val purpose = Purpose.deserialize(requireArguments().getInt(KEY_PURPOSE))
PromptLogsViewModel.Factory(ApplicationDependencies.getApplication(), purpose)
}
)
private lateinit var viewModel: PromptLogsViewModel
private val disposables: LifecycleDisposable = LifecycleDisposable()
@@ -68,21 +55,11 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
disposables.bindTo(viewLifecycleOwner)
val purpose = Purpose.deserialize(requireArguments().getInt(KEY_PURPOSE))
when (purpose) {
Purpose.NOTIFICATIONS -> {
binding.title.setText(R.string.PromptLogsSlowNotificationsDialog__title)
}
Purpose.CRASH -> {
binding.title.setText(R.string.PromptLogsSlowNotificationsDialog__title_crash)
}
}
viewModel = ViewModelProvider(this).get(PromptLogsViewModel::class.java)
binding.submit.setOnClickListener {
val progressDialog = SignalProgressDialog.show(requireContext())
disposables += viewModel.submitLogs().subscribe({ result ->
submitLogs(result, purpose)
submitLogs(result)
progressDialog.dismiss()
dismiss()
}, { _ ->
@@ -91,40 +68,30 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
dismiss()
})
}
binding.decline.setOnClickListener {
if (purpose == Purpose.NOTIFICATIONS) {
SignalStore.uiHints().markDeclinedShareNotificationLogs()
}
SignalStore.uiHints().markDeclinedShareNotificationLogs()
dismiss()
}
}
private fun submitLogs(debugLog: String, purpose: Purpose) {
private fun submitLogs(debugLog: String) {
CommunicationActions.openEmail(
requireContext(),
SupportEmailUtil.getSupportEmailAddress(requireContext()),
getString(R.string.DebugLogsPromptDialogFragment__signal_android_support_request),
getEmailBody(debugLog, purpose)
getEmailBody(debugLog)
)
}
private fun getEmailBody(debugLog: String?, purpose: Purpose): String {
private fun getEmailBody(debugLog: String?): String {
val suffix = StringBuilder()
if (debugLog != null) {
suffix.append("\n")
suffix.append(getString(R.string.HelpFragment__debug_log))
suffix.append(" ")
suffix.append(debugLog)
}
val category = when (purpose) {
Purpose.NOTIFICATIONS -> ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__slow_notifications_category)
Purpose.CRASH -> ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__crash_category)
}
val category = ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__slow_notifications_category)
return SupportEmailUtil.generateSupportEmailBody(
requireContext(),
R.string.DebugLogsPromptDialogFragment__signal_android_support_request,
@@ -133,21 +100,4 @@ class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragmen
suffix.toString()
)
}
enum class Purpose(val serialized: Int) {
NOTIFICATIONS(1), CRASH(2);
companion object {
fun deserialize(serialized: Int): Purpose {
for (value in values()) {
if (value.serialized == serialized) {
return value
}
}
throw IllegalArgumentException("Invalid value: $serialized")
}
}
}
}

View File

@@ -163,10 +163,10 @@ public class LinkPreviewView extends FrameLayout {
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
setLinkPreview(glideRequests, linkPreview, showThumbnail, true, false);
setLinkPreview(glideRequests, linkPreview, showThumbnail, true);
}
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription, boolean scheduleMessageMode) {
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription) {
spinner.setVisibility(GONE);
noPreview.setVisibility(GONE);
@@ -216,7 +216,7 @@ public class LinkPreviewView extends FrameLayout {
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail);
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false);
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
thumbnail.get().showDownloadText(false);
} else if (callLinkRootKey != null) {
thumbnail.setVisibility(VISIBLE);

View File

@@ -5,37 +5,17 @@
package org.thoughtcrime.securesms.components
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.SingleSubject
import org.thoughtcrime.securesms.crash.CrashConfig
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
class PromptLogsViewModel(private val context: Application, purpose: DebugLogsPromptDialogFragment.Purpose) : AndroidViewModel(context) {
class PromptLogsViewModel : ViewModel() {
private val submitDebugLogRepository = SubmitDebugLogRepository()
private val disposables = CompositeDisposable()
init {
if (purpose == DebugLogsPromptDialogFragment.Purpose.CRASH) {
disposables += Single
.fromCallable {
LogDatabase.getInstance(context).crashes.markAsPrompted(CrashConfig.patterns, System.currentTimeMillis())
}
.subscribeOn(Schedulers.io())
.subscribe()
}
}
fun submitLogs(): Single<String> {
val singleSubject = SingleSubject.create<String?>()
submitDebugLogRepository.buildAndSubmitLog { result ->
@@ -48,14 +28,4 @@ class PromptLogsViewModel(private val context: Application, purpose: DebugLogsPr
return singleSubject.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
}
override fun onCleared() {
disposables.clear()
}
class Factory(private val context: Application, private val purpose: DebugLogsPromptDialogFragment.Purpose) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(PromptLogsViewModel(context, purpose))!!
}
}
}

View File

@@ -31,7 +31,6 @@ import org.signal.core.util.logging.Log;
import org.signal.glide.transforms.SignalDownsampleStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequest;
@@ -42,6 +41,7 @@ import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
@@ -80,13 +80,12 @@ public class ThumbnailView extends FrameLayout {
private final CornerMask cornerMask;
private ThumbnailViewTransferControlsState transferControlsState = new ThumbnailViewTransferControlsState();
private ThumbnailViewTransferControlsState transferControlsState = new ThumbnailViewTransferControlsState();
private Stub<TransferControlView> transferControlViewStub;
private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener downloadClickListener = null;
private SlidesClickedListener cancelDownloadClickListener = null;
private SlideClickListener playVideoClickListener = null;
private Slide slide = null;
private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener downloadClickListener = null;
private SlideClickListener progressWheelClickListener = null;
private Slide slide = null;
public ThumbnailView(Context context) {
@@ -368,11 +367,10 @@ public class ThumbnailView extends FrameLayout {
}
transferControlsState = transferControlsState.withSlide(slide)
.withDownloadClickListener(new DownloadClickDispatcher())
.withCancelDownloadClickListener(new CancelClickDispatcher());
.withDownloadClickListener(new DownloadClickDispatcher());
if (MediaUtil.isInstantVideoSupported(slide)) {
transferControlsState = transferControlsState.withInstantPlaybackClickListener(new ProgressWheelClickDispatcher());
if (FeatureFlags.instantVideoPlayback()) {
transferControlsState = transferControlsState.withProgressWheelClickListener(new ProgressWheelClickDispatcher());
}
transferControlsState.applyState(transferControlViewStub);
@@ -527,12 +525,8 @@ public class ThumbnailView extends FrameLayout {
this.downloadClickListener = listener;
}
public void setCancelDownloadClickListener(SlidesClickedListener listener) {
this.cancelDownloadClickListener = listener;
}
public void setPlayVideoClickListener(SlideClickListener listener) {
this.playVideoClickListener = listener;
public void setProgressWheelClickListener(SlideClickListener listener) {
this.progressWheelClickListener = listener;
}
public void clear(GlideRequests glideRequests) {
@@ -574,9 +568,9 @@ public class ThumbnailView extends FrameLayout {
private GlideRequest<Drawable> buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
GlideRequest<Drawable> request = applySizing(glideRequests.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.transition(withCrossFade()));
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.transition(withCrossFade()));
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
@@ -631,7 +625,7 @@ public class ThumbnailView extends FrameLayout {
if (Util.equals(slide, other)) {
if (slide != null && other != null) {
byte[] digestLeft = slide.asAttachment().getDigest();
byte[] digestLeft = slide.asAttachment().getDigest();
byte[] digestRight = other.asAttachment().getDigest();
return Arrays.equals(digestLeft, digestRight);
@@ -676,26 +670,14 @@ public class ThumbnailView extends FrameLayout {
}
}
private class CancelClickDispatcher implements View.OnClickListener {
@Override
public void onClick(View view) {
Log.i(TAG, "onClick() for cancel button");
if (cancelDownloadClickListener != null && slide != null) {
cancelDownloadClickListener.onClick(view, Collections.singletonList(slide));
} else {
Log.w(TAG, "Received a cancel button click, but unable to execute it. slide: " + slide + " cancelDownloadClickListener: " + cancelDownloadClickListener);
}
}
}
private class ProgressWheelClickDispatcher implements View.OnClickListener {
@Override
public void onClick(View view) {
Log.i(TAG, "onClick() for instant video playback");
if (playVideoClickListener != null && slide != null) {
playVideoClickListener.onClick(view, slide);
Log.i(TAG, "onClick() for progress wheel");
if (progressWheelClickListener != null && slide != null) {
progressWheelClickListener.onClick(view, slide);
} else {
Log.w(TAG, "Received an instant video click, but unable to execute it. slide: " + slide + " progressWheelClickListener: " + playVideoClickListener);
Log.w(TAG, "Received a progress wheel click, but unable to execute it. slide: " + slide + " progressWheelClickListener: " + progressWheelClickListener);
}
}
}

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components
import android.view.View.OnClickListener
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.views.Stub
@@ -13,8 +12,7 @@ data class ThumbnailViewTransferControlsState(
val isClickable: Boolean = true,
val slide: Slide? = null,
val downloadClickedListener: OnClickListener? = null,
val cancelDownloadClickedListener: OnClickListener? = null,
val instantPlaybackClickListener: OnClickListener? = null,
val progressWheelClickedListener: OnClickListener? = null,
val showDownloadText: Boolean = true
) {
@@ -22,8 +20,7 @@ data class ThumbnailViewTransferControlsState(
fun withClickable(isClickable: Boolean): ThumbnailViewTransferControlsState = copy(isClickable = isClickable)
fun withSlide(slide: Slide?): ThumbnailViewTransferControlsState = copy(slide = slide)
fun withDownloadClickListener(downloadClickedListener: OnClickListener): ThumbnailViewTransferControlsState = copy(downloadClickedListener = downloadClickedListener)
fun withCancelDownloadClickListener(cancelClickListener: OnClickListener): ThumbnailViewTransferControlsState = copy(cancelDownloadClickedListener = cancelClickListener)
fun withInstantPlaybackClickListener(instantPlaybackClickListener: OnClickListener): ThumbnailViewTransferControlsState = copy(instantPlaybackClickListener = instantPlaybackClickListener)
fun withProgressWheelClickListener(progressWheelClickedListener: OnClickListener): ThumbnailViewTransferControlsState = copy(progressWheelClickedListener = progressWheelClickedListener)
fun withDownloadText(showDownloadText: Boolean): ThumbnailViewTransferControlsState = copy(showDownloadText = showDownloadText)
fun applyState(transferControlView: Stub<TransferControlView>) {
@@ -34,9 +31,8 @@ data class ThumbnailViewTransferControlsState(
transferControlView.get().setSlide(slide)
}
transferControlView.get().setDownloadClickListener(downloadClickedListener)
transferControlView.get().setProgressWheelClickListener(progressWheelClickedListener)
transferControlView.get().setShowDownloadText(showDownloadText)
transferControlView.get().setCancelClickListener(cancelDownloadClickedListener)
transferControlView.get().setInstantPlaybackClickListener(instantPlaybackClickListener)
}
}
}

View File

@@ -0,0 +1,273 @@
package org.thoughtcrime.securesms.components;
import android.animation.LayoutTransition;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.mms.Slide;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public final class TransferControlView extends FrameLayout {
private static final String TAG = "TransferControlView";
private static final int UPLOAD_TASK_WEIGHT = 1;
/**
* A weighting compared to {@link #UPLOAD_TASK_WEIGHT}
*/
private static final int COMPRESSION_TASK_WEIGHT = 3;
@Nullable private List<Slide> slides;
@Nullable private View current;
private final ProgressWheel progressWheel;
private final View downloadDetails;
private final TextView downloadDetailsText;
private final Map<Attachment, Float> networkProgress;
private final Map<Attachment, Float> compresssionProgress;
public TransferControlView(Context context) {
this(context, null);
}
public TransferControlView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TransferControlView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.transfer_controls_view, this);
setLongClickable(false);
setBackground(ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
setVisibility(GONE);
setLayoutTransition(new LayoutTransition());
this.networkProgress = new HashMap<>();
this.compresssionProgress = new HashMap<>();
this.progressWheel = findViewById(R.id.progress_wheel);
this.downloadDetails = findViewById(R.id.download_details);
this.downloadDetailsText = findViewById(R.id.download_details_text);
}
@Override
public void setFocusable(boolean focusable) {
super.setFocusable(focusable);
downloadDetails.setFocusable(focusable);
}
@Override
public void setClickable(boolean clickable) {
super.setClickable(clickable);
downloadDetails.setClickable(clickable);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
EventBus.getDefault().unregister(this);
}
public void setSlide(final @NonNull Slide slides) {
setSlides(Collections.singletonList(slides));
}
public void setSlides(final @NonNull List<Slide> slides) {
if (slides.isEmpty()) {
throw new IllegalArgumentException("Must provide at least one slide.");
}
this.slides = slides;
if (!isUpdateToExistingSet(slides)) {
networkProgress.clear();
compresssionProgress.clear();
Stream.of(slides).forEach(s -> networkProgress.put(s.asAttachment(), 0f));
}
for (Slide slide : slides) {
if (slide.asAttachment().getTransferState() == AttachmentTable.TRANSFER_PROGRESS_DONE) {
networkProgress.put(slide.asAttachment(), 1f);
}
}
switch (getTransferState(slides)) {
case AttachmentTable.TRANSFER_PROGRESS_STARTED:
showProgressSpinner(calculateProgress(networkProgress, compresssionProgress));
break;
case AttachmentTable.TRANSFER_PROGRESS_PENDING:
case AttachmentTable.TRANSFER_PROGRESS_FAILED:
String downloadText = getDownloadText(this.slides);
if (!Objects.equals(downloadText, downloadDetailsText.getText().toString())) {
downloadDetailsText.setText(getDownloadText(this.slides));
}
display(downloadDetails);
break;
default:
display(null);
break;
}
}
public void showProgressSpinner() {
showProgressSpinner(calculateProgress(networkProgress, compresssionProgress));
}
public void showProgressSpinner(float progress) {
if (progress == 0) {
progressWheel.spin();
} else {
progressWheel.setInstantProgress(progress);
}
display(progressWheel);
}
public void setDownloadClickListener(final @Nullable OnClickListener listener) {
downloadDetails.setOnClickListener(listener);
}
public void setProgressWheelClickListener(final @Nullable OnClickListener listener) {
progressWheel.setOnClickListener(listener);
}
public void clear() {
clearAnimation();
setVisibility(GONE);
if (current != null) {
current.clearAnimation();
current.setVisibility(GONE);
}
current = null;
slides = null;
}
public void setShowDownloadText(boolean showDownloadText) {
downloadDetailsText.setVisibility(showDownloadText ? VISIBLE : GONE);
forceLayout();
}
private boolean isUpdateToExistingSet(@NonNull List<Slide> slides) {
if (slides.size() != networkProgress.size()) {
return false;
}
for (Slide slide : slides) {
if (!networkProgress.containsKey(slide.asAttachment())) {
return false;
}
}
return true;
}
static int getTransferState(@NonNull List<Slide> slides) {
int transferState = AttachmentTable.TRANSFER_PROGRESS_DONE;
boolean allFailed = true;
for (Slide slide : slides) {
if (slide.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE) {
allFailed = false;
if (slide.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
transferState = slide.getTransferState();
} else {
transferState = Math.max(transferState, slide.getTransferState());
}
}
}
return allFailed ? AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE : transferState;
}
private String getDownloadText(@NonNull List<Slide> slides) {
if (slides.size() == 1) {
return slides.get(0).getContentDescription(getContext());
} else {
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE ? count + 1 : count);
return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);
}
}
private void display(@Nullable final View view) {
if (current == view) {
return;
}
if (current != null) {
current.setVisibility(GONE);
}
if (view != null) {
view.setVisibility(VISIBLE);
setVisibility(VISIBLE);
} else {
setVisibility(GONE);
}
current = view;
}
private static float calculateProgress(@NonNull Map<Attachment, Float> uploadDownloadProgress, Map<Attachment, Float> compresssionProgress) {
float totalDownloadProgress = 0;
float totalCompressionProgress = 0;
for (float progress : uploadDownloadProgress.values()) {
totalDownloadProgress += progress;
}
for (float progress : compresssionProgress.values()) {
totalCompressionProgress += progress;
}
float weightedProgress = UPLOAD_TASK_WEIGHT * totalDownloadProgress + COMPRESSION_TASK_WEIGHT * totalCompressionProgress;
float weightedTotal = UPLOAD_TASK_WEIGHT * uploadDownloadProgress.size() + COMPRESSION_TASK_WEIGHT * compresssionProgress.size();
return weightedProgress / weightedTotal;
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventAsync(final PartProgressEvent event) {
final Attachment attachment = event.attachment;
if (networkProgress.containsKey(attachment)) {
float proportionCompleted = ((float) event.progress) / event.total;
if (event.type == PartProgressEvent.Type.COMPRESSION) {
compresssionProgress.put(attachment, proportionCompleted);
} else {
networkProgress.put(attachment, proportionCompleted);
}
progressWheel.setInstantProgress(calculateProgress(networkProgress, compresssionProgress));
}
}
}

View File

@@ -32,6 +32,7 @@ import androidx.core.view.GestureDetectorCompat;
import androidx.core.view.ViewKt;
import androidx.core.widget.TextViewCompat;
import org.signal.core.util.StringUtil;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
@@ -311,6 +312,13 @@ public class EmojiTextView extends AppCompatTextView {
setText(previousText, BufferType.SPANNABLE);
}
public void setForceCustomEmoji(boolean forceCustom) {
if (this.forceCustom != forceCustom) {
this.forceCustom = forceCustom;
setText(previousText, BufferType.SPANNABLE);
}
}
@SuppressLint("ClickableViewAccessibility")
public void bindGestureListener() {
GestureDetectorCompat gestureDetectorCompat = new GestureDetectorCompat(getContext(), new OnGestureListener());
@@ -354,10 +362,10 @@ public class EmojiTextView extends AppCompatTextView {
return;
}
int overflowEnd = getLayout().getLineEnd(maxLines);
int overflowEnd = getLayout().getLineEnd(maxLines - 1);
CharSequence overflow = getText().subSequence(overflowStart, overflowEnd);
float adjust = overflowText != null ? getPaint().measureText(overflowText, 0, overflowText.length()) : 0f;
CharSequence ellipsized = TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END);
CharSequence ellipsized = StringUtil.trim(TextUtils.ellipsize(overflow, getPaint(), getWidth() - adjust, TextUtils.TruncateAt.END));
SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, overflowStart).toString())

View File

@@ -4,7 +4,6 @@ import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import okio.ByteString.Companion.toByteString
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.SignalProtocolAddress
@@ -18,6 +17,7 @@ import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.keyvalue.CertificateType
@@ -42,7 +42,7 @@ import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse
import org.whispersystems.signalservice.internal.push.SyncMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
@@ -367,13 +367,13 @@ class ChangeNumberRepository(
// Device Messages
if (deviceId != primaryDeviceId) {
val pniChangeNumber = SyncMessage.PniChangeNumber(
identityKeyPair = pniIdentity.serialize().toByteString(),
signedPreKey = signedPreKeyRecord.serialize().toByteString(),
lastResortKyberPreKey = lastResortKyberPreKeyRecord.serialize().toByteString(),
registrationId = pniRegistrationId,
newE164 = newE164
)
val pniChangeNumber = SyncMessage.PniChangeNumber.newBuilder()
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
.setLastResortKyberPreKey(lastResortKyberPreKeyRecord.serialize().toProtoByteString())
.setRegistrationId(pniRegistrationId)
.setNewE164(newE164)
.build()
deviceMessages += messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId, pniChangeNumber)
}
@@ -391,12 +391,12 @@ class ChangeNumberRepository(
pniRegistrationIds.mapKeys { it.key.toString() }
)
val metadata = PendingChangeNumberMetadata(
previousPni = SignalStore.account().pni!!.toByteString(),
pniIdentityKeyPair = pniIdentity.serialize().toByteString(),
pniRegistrationId = pniRegistrationIds[primaryDeviceId]!!,
pniSignedPreKeyId = devicePniSignedPreKeys[primaryDeviceId]!!.keyId
)
val metadata = PendingChangeNumberMetadata.newBuilder()
.setPreviousPni(SignalStore.account().pni!!.toByteString())
.setPniIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
.setPniRegistrationId(pniRegistrationIds[primaryDeviceId]!!)
.setPniSignedPreKeyId(devicePniSignedPreKeys[primaryDeviceId]!!.keyId)
.build()
return ChangeNumberRequestData(request, metadata)
}

View File

@@ -768,7 +768,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
private fun clearKeepLongerLogs() {
SimpleTask.run({
LogDatabase.getInstance(requireActivity().application).logs.clearKeepLonger()
LogDatabase.getInstance(requireActivity().application).clearKeepLonger()
}) {
Toast.makeText(requireContext(), "Cleared keep longer logs", Toast.LENGTH_SHORT).show()
}

View File

@@ -32,7 +32,7 @@ class InternalSettingsRepository(context: Context) {
val title = "Release Note Title"
val bodyText = "Release note body. Aren't I awesome?"
val body = "$title\n\n$bodyText"
val bodyRangeList = BodyRangeList.Builder()
val bodyRangeList = BodyRangeList.newBuilder()
.addStyle(BodyRangeList.BodyRange.Style.BOLD, 0, title.length)
val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId!!

View File

@@ -10,6 +10,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MediaTable
@@ -151,9 +152,9 @@ class ConversationSettingsRepository(
consumer(
if (groupRecord.isV2Group) {
val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembers
.map { m -> m.serviceIdBytes }
.map { s -> GroupProtoUtil.serviceIdBinaryToRecipientId(s) }
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembersList
.map(DecryptedPendingMember::getServiceIdBytes)
.map(GroupProtoUtil::serviceIdBinaryToRecipientId)
val members = mutableListOf<RecipientId>()

View File

@@ -1,289 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.transfercontrols
import android.animation.LayoutTransition
import android.annotation.SuppressLint
import android.content.Context
import android.text.format.Formatter
import android.util.AttributeSet
import android.view.View
import android.widget.Space
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.constraintlayout.widget.ConstraintLayout
import com.annimon.stream.Stream
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.events.PartProgressEvent
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.ThrottledDebouncer
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
class TransferControlView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
private var slides: List<Slide> = emptyList()
private var current: MutableSet<View> = HashSet()
private var playableWhileDownloading = false
private var showDownloadText = true
private val downloadDetails: View
private val downloadDetailsText: TextView
private val primaryDetailsText: TextView
private val secondaryViewSpace: Space
private val playVideoButton: AppCompatImageView
private val primaryProgressView: TransferProgressView
private val secondaryProgressView: TransferProgressView
private val networkProgress: MutableMap<Attachment, Progress>
private val compressionProgress: MutableMap<Attachment, Progress>
private val debouncer: ThrottledDebouncer = ThrottledDebouncer(8) // frame time for 120 Hz
init {
inflate(context, R.layout.transfer_controls_view, this)
isLongClickable = false
visibility = GONE
layoutTransition = LayoutTransition()
networkProgress = HashMap()
compressionProgress = HashMap()
primaryProgressView = findViewById(R.id.primary_progress_view)
secondaryProgressView = findViewById(R.id.secondary_progress_view)
playVideoButton = findViewById(R.id.play_video_button)
downloadDetails = findViewById(R.id.secondary_background)
downloadDetailsText = findViewById(R.id.download_details_text)
secondaryViewSpace = findViewById(R.id.secondary_view_space)
primaryDetailsText = findViewById(R.id.primary_details_text)
}
override fun setFocusable(focusable: Boolean) {
super.setFocusable(focusable)
progressView.isFocusable = focusable
if (playVideoButton.visibility == VISIBLE) {
playVideoButton.isFocusable = focusable
}
}
override fun setClickable(clickable: Boolean) {
super.setClickable(clickable)
secondaryProgressView.isClickable = secondaryProgressView.visible && clickable
primaryProgressView.isClickable = primaryProgressView.visible && clickable
playVideoButton.isClickable = playVideoButton.visible && clickable
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
EventBus.getDefault().unregister(this)
}
fun setSlide(slides: Slide) {
setSlides(listOf(slides))
}
fun setSlides(slides: List<Slide>) {
require(slides.isNotEmpty()) { "Must provide at least one slide." }
this.slides = slides
if (!isUpdateToExistingSet(slides)) {
networkProgress.clear()
compressionProgress.clear()
slides.forEach { networkProgress[it.asAttachment()] = Progress(0L, it.fileSize) }
}
var allStreamableOrDone = true
for (slide in slides) {
val attachment = slide.asAttachment()
if (attachment.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
networkProgress[attachment] = Progress(1L, attachment.size)
} else if (!MediaUtil.isInstantVideoSupported(slide)) {
allStreamableOrDone = false
}
}
playableWhileDownloading = allStreamableOrDone
setPlayableWhileDownloading(playableWhileDownloading)
val uploading = slides.any { it.asAttachment().uploadTimestamp == 0L }
when (getTransferState(slides)) {
AttachmentTable.TRANSFER_PROGRESS_STARTED -> showProgressSpinner(calculateProgress(), uploading)
AttachmentTable.TRANSFER_PROGRESS_PENDING -> {
updateDownloadText()
progressView.setStopped(false)
this.visible = true
}
AttachmentTable.TRANSFER_PROGRESS_FAILED -> {
downloadDetailsText.setText(R.string.NetworkFailure__retry)
progressView.setStopped(false)
this.visible = true
}
else -> this.visible = false
}
}
private val progressView: TransferProgressView
get() = if (playableWhileDownloading) {
secondaryProgressView
} else {
primaryProgressView
}
@JvmOverloads
fun showProgressSpinner(progress: Float = calculateProgress(), uploading: Boolean = false) {
if (uploading || progress == 0f) {
progressView.setUploading(progress)
} else {
progressView.setDownloading(progress)
}
}
fun setDownloadClickListener(listener: OnClickListener?) {
primaryProgressView.startClickListener = listener
secondaryProgressView.startClickListener = listener
}
fun setCancelClickListener(listener: OnClickListener?) {
primaryProgressView.cancelClickListener = listener
secondaryProgressView.cancelClickListener = listener
}
fun setInstantPlaybackClickListener(onPlayClickedListener: OnClickListener?) {
playVideoButton.setOnClickListener(onPlayClickedListener)
}
fun clear() {
clearAnimation()
visibility = GONE
if (current.isNotEmpty()) {
for (v in current) {
v.clearAnimation()
v.visibility = GONE
}
}
current.clear()
slides = emptyList()
}
fun setShowDownloadText(showDownloadText: Boolean) {
this.showDownloadText = showDownloadText
updateDownloadText()
}
private fun isUpdateToExistingSet(slides: List<Slide>): Boolean {
if (slides.size != networkProgress.size) {
return false
}
for (slide in slides) {
if (!networkProgress.containsKey(slide.asAttachment())) {
return false
}
}
return true
}
private fun updateDownloadText() {
val byteCount = slides.sumOf { it.asAttachment().size }
downloadDetailsText.text = Formatter.formatShortFileSize(context, byteCount)
downloadDetailsText.invalidate()
if (slides.size > 1) {
val downloadCount = Stream.of(slides).reduce(0) { count: Int, slide: Slide -> if (slide.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE) count + 1 else count }
primaryDetailsText.text = context.resources.getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount)
primaryDetailsText.visible = showDownloadText
} else {
primaryDetailsText.text = ""
primaryDetailsText.visible = false
}
}
@SuppressLint("SetTextI18n")
private fun updateDownloadProgressText(isCompression: Boolean) {
val context = context
val progress = if (isCompression) compressionProgress.values.sumOf { it.completed } else networkProgress.values.sumOf { it.completed }
val total = if (isCompression) compressionProgress.values.sumOf { it.total } else networkProgress.values.sumOf { it.total }
val progressText = Formatter.formatShortFileSize(context, progress)
val totalText = Formatter.formatShortFileSize(context, total)
downloadDetailsText.text = "$progressText/$totalText"
downloadDetailsText.invalidate()
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onEventAsync(event: PartProgressEvent) {
val attachment = event.attachment
if (networkProgress.containsKey(attachment)) {
val proportionCompleted = event.progress.toFloat() / event.total
if (event.type == PartProgressEvent.Type.COMPRESSION) {
compressionProgress[attachment] = Progress.fromEvent(event)
} else {
networkProgress[attachment] = Progress.fromEvent(event)
}
debouncer.publish {
val progress = calculateProgress()
if (attachment.uploadTimestamp == 0L) {
progressView.setUploading(progress)
} else {
progressView.setDownloading(progress)
}
updateDownloadProgressText(event.type == PartProgressEvent.Type.COMPRESSION)
}
}
}
private fun setPlayableWhileDownloading(playableWhileDownloading: Boolean) {
playVideoButton.visible = playableWhileDownloading
secondaryProgressView.visible = playableWhileDownloading
secondaryViewSpace.visible = !playableWhileDownloading // exists because constraint layout was being very weird about margins and this was the only way
primaryProgressView.visibility = if (playableWhileDownloading) INVISIBLE else VISIBLE
val textPadding = if (playableWhileDownloading) 0 else context.resources.getDimensionPixelSize(R.dimen.transfer_control_view_progressbar_to_textview_margin)
ViewUtil.setPaddingStart(downloadDetailsText, textPadding)
}
private fun calculateProgress(): Float {
val totalDownloadProgress: Float = networkProgress.values.map { it.completed.toFloat() / it.total }.sum()
val totalCompressionProgress: Float = compressionProgress.values.map { it.completed.toFloat() / it.total }.sum()
val weightedProgress = UPLOAD_TASK_WEIGHT * totalDownloadProgress + COMPRESSION_TASK_WEIGHT * totalCompressionProgress
val weightedTotal = (UPLOAD_TASK_WEIGHT * networkProgress.size + COMPRESSION_TASK_WEIGHT * compressionProgress.size).toFloat()
return weightedProgress / weightedTotal
}
companion object {
private const val TAG = "TransferControlView"
private const val UPLOAD_TASK_WEIGHT = 1
/**
* A weighting compared to [.UPLOAD_TASK_WEIGHT]
*/
private const val COMPRESSION_TASK_WEIGHT = 3
@JvmStatic
fun getTransferState(slides: List<Slide>): Int {
var transferState = AttachmentTable.TRANSFER_PROGRESS_DONE
var allFailed = true
for (slide in slides) {
if (slide.transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE) {
allFailed = false
transferState = if (slide.transferState == AttachmentTable.TRANSFER_PROGRESS_PENDING && transferState == AttachmentTable.TRANSFER_PROGRESS_DONE) {
slide.transferState
} else {
transferState.coerceAtLeast(slide.transferState)
}
}
}
return if (allFailed) AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE else transferState
}
}
data class Progress(val completed: Long, val total: Long) {
companion object {
fun fromEvent(event: PartProgressEvent): Progress {
return Progress(event.progress, event.total)
}
}
}
}

View File

@@ -1,169 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.transfercontrols
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.withTranslation
import org.signal.core.util.dp
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import kotlin.math.roundToInt
class TransferProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : View(context, attrs, defStyleAttr, defStyleRes) {
companion object {
const val TAG = "TransferProgressView"
private const val PROGRESS_ARC_STROKE_WIDTH = 2f
private const val ICON_INSET_PERCENT = 0.2f
}
private val progressRect = RectF()
private val stopIconRect = RectF()
private val progressPaint = progressPaint()
private val stopIconPaint = stopIconPaint()
private val trackPaint = trackPaint()
private var progressPercent = 0f
private var currentState = State.READY_TO_DOWNLOAD
private val downloadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_24)
private val uploadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up_16)
var startClickListener: OnClickListener? = null
var cancelClickListener: OnClickListener? = null
init {
val tint = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
val filter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_ATOP)
downloadDrawable?.colorFilter = filter
uploadDrawable?.colorFilter = filter
}
override fun requestLayout() {
super.requestLayout()
Log.d(TAG, "Requesting new layout.", Exception())
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
when (currentState) {
State.IN_PROGRESS_CANCELABLE, State.IN_PROGRESS_NON_CANCELABLE -> drawProgress(canvas, progressPercent)
State.READY_TO_UPLOAD -> sizeAndDrawDrawable(canvas, uploadDrawable)
State.READY_TO_DOWNLOAD -> sizeAndDrawDrawable(canvas, downloadDrawable)
}
}
fun setDownloading(progress: Float) {
if (currentState != State.IN_PROGRESS_CANCELABLE) {
currentState = State.IN_PROGRESS_CANCELABLE
setOnClickListener(cancelClickListener)
}
progressPercent = progress
}
fun setUploading(progress: Float) {
if (currentState != State.IN_PROGRESS_NON_CANCELABLE) {
currentState = State.IN_PROGRESS_NON_CANCELABLE
setOnClickListener(null)
}
progressPercent = progress
}
fun setStopped(isUpload: Boolean) {
val newState = if (isUpload) State.READY_TO_UPLOAD else State.READY_TO_DOWNLOAD
if (currentState != newState) {
currentState = newState
setOnClickListener(startClickListener)
}
progressPercent = 0f
}
private fun drawProgress(canvas: Canvas, progressPercent: Float) {
if (currentState == State.IN_PROGRESS_CANCELABLE) {
val miniIcon = height < 32.dp
val stopIconCornerRadius = if (miniIcon) 1f.dp else 4f.dp
val iconSize: Float = if (miniIcon) 6.6f.dp else 16f.dp
stopIconRect.set(0f, 0f, iconSize, iconSize)
canvas.withTranslation(width / 2 - (iconSize / 2), height / 2 - (iconSize / 2)) {
drawRoundRect(stopIconRect, stopIconCornerRadius, stopIconCornerRadius, stopIconPaint)
}
}
val widthDp = PROGRESS_ARC_STROKE_WIDTH.dp
val inset = 2.dp
progressRect.top = widthDp + inset
progressRect.left = widthDp + inset
progressRect.right = (width - widthDp) - inset
progressRect.bottom = (height - widthDp) - inset
canvas.drawArc(progressRect, 0f, 360f, false, trackPaint)
canvas.drawArc(progressRect, 270f, 360f * progressPercent, false, progressPaint)
}
private fun stopIconPaint(): Paint {
val stopIconPaint = Paint()
stopIconPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
stopIconPaint.isAntiAlias = true
stopIconPaint.style = Paint.Style.FILL
return stopIconPaint
}
private fun trackPaint(): Paint {
val trackPaint = Paint()
trackPaint.color = ContextCompat.getColor(context, R.color.signal_colorTransparent2)
trackPaint.isAntiAlias = true
trackPaint.style = Paint.Style.STROKE
trackPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp
return trackPaint
}
private fun progressPaint(): Paint {
val progressPaint = Paint()
progressPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
progressPaint.isAntiAlias = true
progressPaint.style = Paint.Style.STROKE
progressPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp
return progressPaint
}
private fun sizeAndDrawDrawable(canvas: Canvas, drawable: Drawable?) {
if (drawable == null) {
Log.w(TAG, "Could not load icon for $currentState")
return
}
drawable.setBounds(
(width * ICON_INSET_PERCENT).roundToInt(),
(height * ICON_INSET_PERCENT).roundToInt(),
(width * (1 - ICON_INSET_PERCENT)).roundToInt(),
(height * (1 - ICON_INSET_PERCENT)).roundToInt()
)
drawable.draw(canvas)
}
private enum class State {
IN_PROGRESS_CANCELABLE,
IN_PROGRESS_NON_CANCELABLE,
READY_TO_UPLOAD,
READY_TO_DOWNLOAD
}
}

View File

@@ -113,10 +113,18 @@ public class VoiceNotePlaybackService extends MediaSessionService {
private class VoiceNotePlayerEventListener implements Player.Listener {
private int previousPlaybackState = player.getPlaybackState();
@Override
public void onPlayWhenReadyChanged(boolean playWhenReady, int reason) {
onPlaybackStateChanged(playWhenReady, player.getPlaybackState());
}
@Override
public void onPlaybackStateChanged(int playbackState) {
Log.d(TAG, "[onPlaybackStateChanged] playbackState: " + playbackState);
boolean playWhenReady = player.getPlayWhenReady();
onPlaybackStateChanged(player.getPlayWhenReady(), playbackState);
}
private void onPlaybackStateChanged(boolean playWhenReady, int playbackState) {
Log.d(TAG, "playWhenReady: " + playWhenReady + "\nplaybackState: " + playbackState);
switch (playbackState) {
case Player.STATE_BUFFERING:
case Player.STATE_READY:
@@ -162,10 +170,9 @@ public class VoiceNotePlaybackService extends MediaSessionService {
player.seekTo(mediaItemIndex, 1);
player.setPlayWhenReady(true);
}
} else if (reason == Player.DISCONTINUITY_REASON_SEEK) {
player.setPlayWhenReady(true);
}
boolean isWithinThreshold = mediaItemIndex < LOAD_MORE_THRESHOLD ||
mediaItemIndex + LOAD_MORE_THRESHOLD >= player.getMediaItemCount();

View File

@@ -102,8 +102,6 @@ class CallLinkInfoSheet : ComposeBottomSheetDialogFragment() {
}
}
override fun isDarkTheme(): Boolean = true
private val webRtcCallViewModel: WebRtcCallViewModel by activityViewModels()
private val callLinkDetailsViewModel: CallLinkDetailsViewModel by viewModels(factoryProducer = {
CallLinkDetailsViewModel.Factory(BundleCompat.getParcelable(requireArguments(), CALL_LINK_ROOM_ID, CallLinkRoomId::class.java)!!)

View File

@@ -53,8 +53,6 @@ class PendingParticipantsView @JvmOverloads constructor(
val firstRecipient: Recipient = unresolvedPendingParticipants.first()
avatar.setAvatar(firstRecipient)
avatar.setOnClickListener { listener?.onLaunchRecipientSheet(firstRecipient) }
name.text = firstRecipient.getShortDisplayName(context)
allow.setOnClickListener { listener?.onAllowPendingRecipient(firstRecipient) }
@@ -72,11 +70,6 @@ class PendingParticipantsView @JvmOverloads constructor(
}
interface Listener {
/**
* Display the sheet containing the request for the top level participant
*/
fun onLaunchRecipientSheet(pendingRecipient: Recipient)
/**
* Given recipient should be admitted to the call
*/

View File

@@ -30,7 +30,7 @@ class WebRtcAudioPicker31(private val audioOutputChangedListener: OnAudioOutputC
return null
}
val devices: List<AudioOutputOption> = am.availableCommunicationDevices.map { AudioOutputOption(it.toFriendlyName(fragmentActivity).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }.distinctBy { it.deviceType.name }.filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
val devices: List<AudioOutputOption> = am.availableCommunicationDevices.map { AudioOutputOption(it.toFriendlyName(fragmentActivity).toString(), AudioDeviceMapping.fromPlatformType(it.type), it.id) }.distinct().filterNot { it.deviceType == SignalAudioManager.AudioDevice.NONE }
val currentDeviceId = am.communicationDevice?.id ?: -1
if (devices.size < threshold) {
Log.d(TAG, "Only found $devices devices,\nnot showing picker.")

View File

@@ -600,15 +600,7 @@ public class WebRtcCallView extends ConstraintLayout {
public void setStatus(@Nullable String status) {
ThreadUtil.assertMainThread();
this.status.setText(status);
try {
// Toolbar's subtitle view sometimes already has a parent somehow,
// so we clear it out first so that it removes the view from its parent.
// In addition, we catch the ISE to prevent a crash.
collapsedToolbar.setSubtitle(null);
collapsedToolbar.setSubtitle(status);
} catch (IllegalStateException e) {
Log.w(TAG, "IllegalStateException trying to set status on collapsed Toolbar.");
}
collapsedToolbar.setSubtitle(status);
}
private void setStatus(@StringRes int statusRes) {

View File

@@ -1,31 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.requests
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.contacts.paged.GroupsInCommon
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
class CallLinkIncomingRequestRepository {
fun getGroupsInCommon(recipientId: RecipientId): Observable<GroupsInCommon> {
return Recipient.observable(recipientId).flatMapSingle { recipient ->
if (recipient.hasGroupsInCommon()) {
Single.fromCallable {
val groupsInCommon = SignalDatabase.groups.getGroupsContainingMember(recipient.id, true)
val total = groupsInCommon.size
val names = groupsInCommon.take(2).map { it.title!! }
GroupsInCommon(total, names)
}.observeOn(Schedulers.io())
} else {
Single.just(GroupsInCommon(0, listOf()))
}
}
}
}

View File

@@ -1,235 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.requests
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rxjava3.subscribeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Dividers
import org.signal.core.ui.Rows
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.viewModel
/**
* Displayed when the user presses the user avatar in the call link join request
* bar.
*/
class CallLinkIncomingRequestSheet : ComposeBottomSheetDialogFragment() {
companion object {
private const val RECIPIENT_ID = "recipient_id"
@JvmStatic
fun show(fragmentManager: FragmentManager, recipientId: RecipientId) {
CallLinkIncomingRequestSheet().apply {
arguments = bundleOf(
RECIPIENT_ID to recipientId
)
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
override fun isDarkTheme(): Boolean = true
private val recipientId: RecipientId by lazy {
requireArguments().getParcelableCompat(RECIPIENT_ID, RecipientId::class.java)!!
}
private val viewModel by viewModel {
CallLinkIncomingRequestViewModel(recipientId)
}
@Composable
override fun SheetContent() {
val state = viewModel.observeState(LocalContext.current).subscribeAsState(initial = CallLinkIncomingRequestState())
if (state.value.recipient == Recipient.UNKNOWN) {
return
}
CallLinkIncomingRequestSheetContent(
state = state.value,
onApproveEntry = this::onApproveEntry,
onDenyEntry = this::onDenyEntry
)
}
private fun onApproveEntry() {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(recipientId)
dismissAllowingStateLoss()
}
private fun onDenyEntry() {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(recipientId)
dismissAllowingStateLoss()
}
}
@Preview
@Composable
private fun CallLinkIncomingRequestSheetContentPreview() {
SignalTheme(isDarkMode = true) {
Surface {
CallLinkIncomingRequestSheetContent(
state = CallLinkIncomingRequestState(
name = "Miles Morales",
subtitle = "+1 (555) 555-5555",
groupsInCommon = "Member of Webheads",
isSystemContact = true
),
onApproveEntry = {},
onDenyEntry = {}
)
}
}
}
@Composable
private fun CallLinkIncomingRequestSheetContent(
state: CallLinkIncomingRequestState,
onApproveEntry: () -> Unit,
onDenyEntry: () -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item { BottomSheets.Handle() }
item { Avatar(state.recipient) }
item {
Title(
recipientName = state.name,
isSystemContact = state.isSystemContact
)
}
if (state.subtitle.isNotEmpty()) {
item {
Text(
text = state.subtitle,
modifier = Modifier.padding(4.dp)
)
}
}
if (state.groupsInCommon.isNotEmpty()) {
item {
Text(
text = state.groupsInCommon,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(6.dp)
)
}
}
item {
Dividers.Default()
}
item {
Rows.TextRow(
text = stringResource(id = R.string.CallLinkIncomingRequestSheet__approve_entry),
icon = ImageVector.vectorResource(R.drawable.symbol_check_circle_24),
onClick = onApproveEntry
)
}
item {
Rows.TextRow(
text = stringResource(id = R.string.CallLinkIncomingRequestSheet__deny_entry),
icon = ImageVector.vectorResource(R.drawable.symbol_x_circle_24),
onClick = onDenyEntry
)
}
item {
Spacer(modifier = Modifier.size(32.dp))
}
}
}
@Composable
private fun Avatar(
recipient: Recipient
) {
if (LocalInspectionMode.current) {
Spacer(
modifier = Modifier
.padding(top = 13.dp)
.size(80.dp)
.background(color = Color.Red, shape = CircleShape)
)
} else {
AndroidView(
factory = ::AvatarImageView,
modifier = Modifier
.size(80.dp)
.padding(top = 13.dp)
) {
it.setAvatarUsingProfile(recipient)
}
}
}
@Composable
private fun Title(
recipientName: String,
isSystemContact: Boolean
) {
if (isSystemContact) {
Row(modifier = Modifier.padding(top = 12.dp)) {
Text(
text = recipientName,
style = MaterialTheme.typography.headlineMedium
)
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_person_circle_24),
contentDescription = null,
modifier = Modifier
.padding(start = 6.dp)
.align(CenterVertically)
)
}
} else {
Text(
text = recipientName,
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -1,17 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.requests
import androidx.compose.runtime.Stable
import org.thoughtcrime.securesms.recipients.Recipient
data class CallLinkIncomingRequestState(
val recipient: Recipient = Recipient.UNKNOWN,
val name: String = "",
val isSystemContact: Boolean = false,
val subtitle: String = "",
@Stable val groupsInCommon: String = ""
)

View File

@@ -1,48 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.requests
import android.content.Context
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.rx.RxStore
class CallLinkIncomingRequestViewModel(
private val recipientId: RecipientId
) : ViewModel() {
private val repository = CallLinkIncomingRequestRepository()
private val store = RxStore(CallLinkIncomingRequestState())
private val disposables = CompositeDisposable().apply {
add(store)
}
override fun onCleared() {
disposables.dispose()
}
fun observeState(context: Context): Flowable<CallLinkIncomingRequestState> {
disposables += store.update(Recipient.observable(recipientId).toFlowable(BackpressureStrategy.LATEST)) { r, s ->
s.copy(
recipient = r,
name = r.getShortDisplayName(context),
subtitle = r.e164.orElse(""),
isSystemContact = r.isSystemContact
)
}
disposables += store.update(repository.getGroupsInCommon(recipientId).toFlowable(BackpressureStrategy.LATEST)) { g, s ->
s.copy(groupsInCommon = g.toDisplayText(context))
}
return store.stateFlowable
}
}

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import org.signal.core.ui.theme.SignalTheme
@@ -16,14 +17,12 @@ import org.thoughtcrime.securesms.util.DynamicTheme
abstract class ComposeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
protected open fun isDarkTheme(): Boolean = DynamicTheme.isDarkTheme(requireContext())
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
SignalTheme(
isDarkMode = isDarkTheme()
isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)
) {
Surface(shape = RoundedCornerShape(18.dp, 18.dp)) {
SheetContent()

View File

@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversationlist.chatfilter.ConversationFilterRequest
import org.thoughtcrime.securesms.groups.SelectionLimits
@@ -51,10 +50,6 @@ class ContactSearchMediator(
arbitraryRepository: ArbitraryRepository? = null
) {
companion object {
private val TAG = Log.tag(ContactSearchMediator::class.java)
}
private val queryDebouncer = Debouncer(300, TimeUnit.MILLISECONDS)
private val viewModel: ContactSearchViewModel = ViewModelProvider(
@@ -75,17 +70,14 @@ class ContactSearchMediator(
displayOptions = displayOptions,
callbacks = object : ContactSearchAdapter.ClickCallbacks {
override fun onStoryClicked(view: View, story: ContactSearchData.Story, isSelected: Boolean) {
Log.d(TAG, "onStoryClicked() Recipient: ${story.recipient.id}")
toggleStorySelection(view, story, isSelected)
}
override fun onKnownRecipientClicked(view: View, knownRecipient: ContactSearchData.KnownRecipient, isSelected: Boolean) {
Log.d(TAG, "onKnownRecipientClicked() Recipient: ${knownRecipient.recipient.id}")
toggleSelection(view, knownRecipient, isSelected)
}
override fun onExpandClicked(expand: ContactSearchData.Expand) {
Log.d(TAG, "onExpandClicked()")
viewModel.expandSection(expand.sectionKey)
}
},
@@ -127,7 +119,6 @@ class ContactSearchMediator(
}
fun setKeysSelected(keys: Set<ContactSearchKey>) {
Log.d(TAG, "setKeysSelected() Keys: ${keys.map { it.toString() }}")
viewModel.setKeysSelected(callbacks.onBeforeContactsSelected(null, keys))
}
@@ -176,11 +167,9 @@ class ContactSearchMediator(
private fun toggleSelection(view: View, contactSearchData: ContactSearchData, isSelected: Boolean) {
return if (isSelected) {
Log.d(TAG, "toggleSelection(OFF) ${contactSearchData.contactSearchKey}")
callbacks.onContactDeselected(view, contactSearchData.contactSearchKey)
viewModel.setKeysNotSelected(setOf(contactSearchData.contactSearchKey))
} else {
Log.d(TAG, "toggleSelection(ON) ${contactSearchData.contactSearchKey}")
viewModel.setKeysSelected(callbacks.onBeforeContactsSelected(view, setOf(contactSearchData.contactSearchKey)))
}
}
@@ -223,13 +212,10 @@ class ContactSearchMediator(
open class SimpleCallbacks : Callbacks {
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey> {
Log.d(TAG, "onBeforeContactsSelected() Selecting: ${contactSearchKeys.map { it.toString() }}")
return contactSearchKeys
}
override fun onContactDeselected(view: View?, contactSearchKey: ContactSearchKey) {
Log.i(TAG, "onContactDeselected() Deselected: $contactSearchKey}")
}
override fun onContactDeselected(view: View?, contactSearchKey: ContactSearchKey) = Unit
override fun onAdapterListCommitted(size: Int) = Unit
}

View File

@@ -57,13 +57,9 @@ object ContactDiscovery {
}
if (!SignalStore.registrationValues().isRegistrationComplete) {
if (SignalStore.account().isRegistered && SignalStore.svr().lastPinCreateFailed()) {
Log.w(TAG, "Registration isn't complete, but only because PIN creation failed. Allowing CDS to continue.")
} else {
Log.w(TAG, "Registration is not yet complete. Skipping, but running a routine to possibly mark it complete.")
RegistrationUtil.maybeMarkRegistrationComplete()
return
}
Log.w(TAG, "Registration is not yet complete. Skipping, but running a routine to possibly mark it complete.")
RegistrationUtil.maybeMarkRegistrationComplete()
return
}
refreshRecipients(

View File

@@ -9,7 +9,7 @@ import org.whispersystems.signalservice.api.InvalidMessageStructureException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil;
import org.whispersystems.signalservice.internal.push.DataMessage;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.util.ArrayList;
import java.util.LinkedList;
@@ -125,47 +125,47 @@ public class ContactModelMapper {
return new Contact(name, sharedContact.getOrganization().orElse(null), phoneNumbers, emails, postalAddresses, avatar);
}
public static Contact remoteToLocal(@NonNull DataMessage.Contact contact) {
Name name = new Name(contact.name.displayName,
contact.name.givenName,
contact.name.familyName,
contact.name.prefix,
contact.name.suffix,
contact.name.middleName);
public static Contact remoteToLocal(@NonNull SignalServiceProtos.DataMessage.Contact contact) {
Name name = new Name(contact.getName().getDisplayName(),
contact.getName().getGivenName(),
contact.getName().getFamilyName(),
contact.getName().getPrefix(),
contact.getName().getSuffix(),
contact.getName().getMiddleName());
List<Phone> phoneNumbers = new ArrayList<>(contact.number.size());
for (DataMessage.Contact.Phone phone : contact.number) {
phoneNumbers.add(new Phone(phone.value_,
remoteToLocalType(phone.type),
phone.label));
List<Phone> phoneNumbers = new ArrayList<>(contact.getNumberCount());
for (SignalServiceProtos.DataMessage.Contact.Phone phone : contact.getNumberList()) {
phoneNumbers.add(new Phone(phone.getValue(),
remoteToLocalType(phone.getType()),
phone.getLabel()));
}
List<Email> emails = new ArrayList<>(contact.email.size());
for (DataMessage.Contact.Email email : contact.email) {
emails.add(new Email(email.value_,
remoteToLocalType(email.type),
email.label));
List<Email> emails = new ArrayList<>(contact.getEmailCount());
for (SignalServiceProtos.DataMessage.Contact.Email email : contact.getEmailList()) {
emails.add(new Email(email.getValue(),
remoteToLocalType(email.getType()),
email.getLabel()));
}
List<PostalAddress> postalAddresses = new ArrayList<>(contact.address.size());
for (DataMessage.Contact.PostalAddress postalAddress : contact.address) {
postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.type),
postalAddress.label,
postalAddress.street,
postalAddress.pobox,
postalAddress.neighborhood,
postalAddress.city,
postalAddress.region,
postalAddress.postcode,
postalAddress.country));
List<PostalAddress> postalAddresses = new ArrayList<>(contact.getAddressCount());
for (SignalServiceProtos.DataMessage.Contact.PostalAddress postalAddress : contact.getAddressList()) {
postalAddresses.add(new PostalAddress(remoteToLocalType(postalAddress.getType()),
postalAddress.getLabel(),
postalAddress.getStreet(),
postalAddress.getPobox(),
postalAddress.getNeighborhood(),
postalAddress.getCity(),
postalAddress.getRegion(),
postalAddress.getPostcode(),
postalAddress.getCountry()));
}
Avatar avatar = null;
if (contact.avatar != null) {
if (contact.hasAvatar()) {
try {
SignalServiceAttachmentPointer attachmentPointer = AttachmentPointerUtil.createSignalAttachmentPointer(contact.avatar.avatar);
SignalServiceAttachmentPointer attachmentPointer = AttachmentPointerUtil.createSignalAttachmentPointer(contact.getAvatar().getAvatar());
Attachment attachment = PointerAttachment.forPointer(Optional.of(attachmentPointer.asPointer())).get();
boolean isProfile = contact.avatar.isProfile;
boolean isProfile = contact.getAvatar().getIsProfile();
avatar = new Avatar(null, attachment, isProfile);
} catch (InvalidMessageStructureException e) {
@@ -173,7 +173,7 @@ public class ContactModelMapper {
}
}
return new Contact(name, contact.organization, phoneNumbers, emails, postalAddresses, avatar);
return new Contact(name, contact.getOrganization(), phoneNumbers, emails, postalAddresses, avatar);
}
private static Phone.Type remoteToLocalType(SharedContact.Phone.Type type) {
@@ -185,7 +185,7 @@ public class ContactModelMapper {
}
}
private static Phone.Type remoteToLocalType(DataMessage.Contact.Phone.Type type) {
private static Phone.Type remoteToLocalType(SignalServiceProtos.DataMessage.Contact.Phone.Type type) {
switch (type) {
case HOME: return Phone.Type.HOME;
case MOBILE: return Phone.Type.MOBILE;
@@ -203,7 +203,7 @@ public class ContactModelMapper {
}
}
private static Email.Type remoteToLocalType(DataMessage.Contact.Email.Type type) {
private static Email.Type remoteToLocalType(SignalServiceProtos.DataMessage.Contact.Email.Type type) {
switch (type) {
case HOME: return Email.Type.HOME;
case MOBILE: return Email.Type.MOBILE;
@@ -220,7 +220,7 @@ public class ContactModelMapper {
}
}
private static PostalAddress.Type remoteToLocalType(DataMessage.Contact.PostalAddress.Type type) {
private static PostalAddress.Type remoteToLocalType(SignalServiceProtos.DataMessage.Contact.PostalAddress.Type type) {
switch (type) {
case HOME: return PostalAddress.Type.HOME;
case WORK: return PostalAddress.Type.WORK;

View File

@@ -114,7 +114,8 @@ public class ConversationAdapter
private ConversationMessage inlineContent;
private Colorizer colorizer;
private boolean isTypingViewEnabled;
private ConversationItemDisplayMode displayMode;
private ConversationItemDisplayMode condensedMode;
private boolean scheduledMessagesMode;
private PulseRequest pulseRequest;
public ConversationAdapter(@NonNull Context context,
@@ -251,7 +252,12 @@ public class ConversationAdapter
}
public void setCondensedMode(ConversationItemDisplayMode condensedMode) {
this.displayMode = condensedMode;
this.condensedMode = condensedMode;
notifyDataSetChanged();
}
public void setScheduledMessagesMode(boolean scheduledMessagesMode) {
this.scheduledMessagesMode = scheduledMessagesMode;
notifyDataSetChanged();
}
@@ -270,7 +276,7 @@ public class ConversationAdapter
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
ConversationItemDisplayMode itemDisplayMode = displayMode != null ? displayMode : ConversationItemDisplayMode.Standard.INSTANCE;
ConversationItemDisplayMode displayMode = condensedMode != null ? condensedMode : ConversationItemDisplayMode.STANDARD;
conversationViewHolder.getBindable().bind(lifecycleOwner,
conversationMessage,
@@ -282,11 +288,11 @@ public class ConversationAdapter
conversationMessage.getThreadRecipient(),
searchQuery,
conversationMessage == recordToPulse,
hasWallpaper && itemDisplayMode.displayWallpaper(),
hasWallpaper && displayMode.displayWallpaper(),
isMessageRequestAccepted,
conversationMessage == inlineContent,
colorizer,
itemDisplayMode);
displayMode);
if (conversationMessage == recordToPulse) {
recordToPulse = null;
@@ -325,9 +331,9 @@ public class ConversationAdapter
if (conversationMessage == null) return -1;
if (displayMode.getScheduleMessageMode()) {
if (scheduledMessagesMode) {
calendar.setTimeInMillis(((MediaMmsMessageRecord) conversationMessage.getMessageRecord()).getScheduledDate());
} else if (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
} else if (condensedMode == ConversationItemDisplayMode.EDIT_HISTORY) {
calendar.setTimeInMillis(conversationMessage.getMessageRecord().getDateSent());
} else {
calendar.setTimeInMillis(conversationMessage.getConversationTimestamp());
@@ -345,9 +351,9 @@ public class ConversationAdapter
Context context = viewHolder.itemView.getContext();
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
if (displayMode.getScheduleMessageMode()) {
if (scheduledMessagesMode) {
viewHolder.setText(DateUtils.getScheduledMessagesDateHeaderString(viewHolder.itemView.getContext(), locale, ((MediaMmsMessageRecord) conversationMessage.getMessageRecord()).getScheduledDate()));
} else if (displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
} else if (condensedMode == ConversationItemDisplayMode.EDIT_HISTORY) {
viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateSent()));
} else {
viewHolder.setText(DateUtils.getConversationDateHeaderString(viewHolder.itemView.getContext(), locale, conversationMessage.getConversationTimestamp()));

View File

@@ -63,16 +63,12 @@ import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.collect.Sets;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.StringUtil;
import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.BindableConversationItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.badges.gifts.GiftMessageView;
@@ -110,10 +106,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
import org.thoughtcrime.securesms.jobs.MmsSendJob;
@@ -247,8 +241,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
private final AttachmentDownloadClickListener downloadClickListener = new AttachmentDownloadClickListener();
private final PlayVideoClickListener playVideoClickListener = new PlayVideoClickListener();
private final AttachmentCancelClickListener attachmentCancelClickListener = new AttachmentCancelClickListener();
private final ProgressWheelClickListener progressWheelClickListener = new ProgressWheelClickListener();
private final SlideClickPassthroughListener singleDownloadClickListener = new SlideClickPassthroughListener(downloadClickListener);
private final SharedContactEventListener sharedContactEventListener = new SharedContactEventListener();
private final SharedContactClickListener sharedContactClickListener = new SharedContactClickListener();
@@ -526,8 +519,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
return;
}
reactionsView.setBubbleWidth(bodyBubble.getWidth());
boolean needsMeasure = false;
if (hasQuote(messageRecord)) {
@@ -937,7 +928,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
* Today this is only {@link org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet}.
*/
private boolean isCondensedMode() {
return displayMode instanceof ConversationItemDisplayMode.Condensed;
return displayMode == ConversationItemDisplayMode.CONDENSED;
}
/**
@@ -1067,10 +1058,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (conversationMessage.getBottomButton() != null) {
callToActionStub.get().setVisibility(View.VISIBLE);
callToActionStub.get().setText(conversationMessage.getBottomButton().label);
callToActionStub.get().setText(conversationMessage.getBottomButton().getLabel());
callToActionStub.get().setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onCallToAction(conversationMessage.getBottomButton().action);
eventListener.onCallToAction(conversationMessage.getBottomButton().getAction());
}
});
} else if (callToActionStub.resolved()) {
@@ -1162,8 +1153,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
if (callLinkRootKey != null) {
joinCallLinkStub.setVisibility(View.VISIBLE);
joinCallLinkStub.get().setTextColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorPrimary);
joinCallLinkStub.get().setStrokeColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorOutline);
joinCallLinkStub.get().setTextColor(messageRecord.isOutgoing() ? R.color.signal_colorOnCustom : R.color.signal_colorOnSurface);
joinCallLinkStub.get().setJoinClickListener(v -> {
if (eventListener != null) {
eventListener.onJoinCallLink(callLinkRootKey);
@@ -1179,8 +1169,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
mediaThumbnailStub.require().setImageResource(glideRequests, Collections.singletonList(new ImageSlide(linkPreview.getThumbnail().get())), showControls, false);
mediaThumbnailStub.require().setThumbnailClickListener(new LinkPreviewThumbnailClickListener());
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
mediaThumbnailStub.require().setProgressWheelClickListener(progressWheelClickListener);
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, false);
@@ -1192,7 +1181,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.setTopMargin(linkPreviewStub.get(), 0);
} else {
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true, !isContentCondensed(), displayMode.getScheduleMessageMode());
linkPreviewStub.get().setLinkPreview(glideRequests, linkPreview, true, !isContentCondensed());
linkPreviewStub.get().setDownloadClickedListener(downloadClickListener);
setLinkPreviewCorners(messageRecord, previousRecord, nextRecord, isGroupThread, false);
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
@@ -1252,7 +1241,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
documentViewStub.get().setDocument(
((MediaMmsMessageRecord) messageRecord).getSlideDeck().getDocumentSlide(),
showControls,
displayMode != ConversationItemDisplayMode.Detailed.INSTANCE
displayMode != ConversationItemDisplayMode.DETAILED
);
documentViewStub.get().setDocumentClickListener(new ThumbnailClickListener());
documentViewStub.get().setDownloadClickListener(singleDownloadClickListener);
@@ -1320,8 +1309,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
false);
mediaThumbnailStub.require().setThumbnailClickListener(new ThumbnailClickListener());
mediaThumbnailStub.require().setDownloadClickListener(downloadClickListener);
mediaThumbnailStub.require().setCancelDownloadClickListener(attachmentCancelClickListener);
mediaThumbnailStub.require().setPlayVideoClickListener(playVideoClickListener);
mediaThumbnailStub.require().setProgressWheelClickListener(progressWheelClickListener);
mediaThumbnailStub.require().setOnLongClickListener(passthroughClickListener);
mediaThumbnailStub.require().setOnClickListener(passthroughClickListener);
mediaThumbnailStub.require().showShade(messageRecord.isDisplayBodyEmpty(getContext()) && !hasExtraText(messageRecord));
@@ -1775,7 +1763,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
private void setHasBeenQuoted(@NonNull ConversationMessage message) {
if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EditHistory.INSTANCE) {
if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EDIT_HISTORY) {
quotedIndicator.setVisibility(VISIBLE);
quotedIndicator.setOnClickListener(quotedIndicatorClickListener);
} else if (quotedIndicator != null) {
@@ -1798,11 +1786,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
private boolean forceFooter(@NonNull MessageRecord messageRecord) {
return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE;
return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord) || displayMode == ConversationItemDisplayMode.EDIT_HISTORY;
}
private boolean forceGroupHeader(@NonNull MessageRecord messageRecord) {
return displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE;
return displayMode == ConversationItemDisplayMode.EDIT_HISTORY;
}
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
@@ -1906,7 +1894,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
int background;
if (isSingularMessage(current, previous, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
if (isSingularMessage(current, previous, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EDIT_HISTORY) {
if (current.isOutgoing()) {
background = R.drawable.message_bubble_background_sent_alone;
outliner.setRadius(bigRadius);
@@ -2002,11 +1990,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
int spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_collapse);
int spacingBottom = spacingTop;
if (isStartOfMessageCluster(current, previous, isGroupThread) && (displayMode != ConversationItemDisplayMode.EditHistory.INSTANCE || next.isEmpty())) {
if (isStartOfMessageCluster(current, previous, isGroupThread) && (displayMode != ConversationItemDisplayMode.EDIT_HISTORY || next.isEmpty())) {
spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_default);
}
if (isEndOfMessageCluster(current, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EditHistory.INSTANCE) {
if (isEndOfMessageCluster(current, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EDIT_HISTORY) {
spacingBottom = readDimen(context, R.dimen.conversation_vertical_message_spacing_default);
}
@@ -2452,84 +2440,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
private class AttachmentCancelClickListener implements SlidesClickedListener {
@Override
public void onClick(View v, List<Slide> slides) {
Log.i(TAG, "onClick() for attachment cancellation");
final JobManager jobManager = ApplicationDependencies.getJobManager();
if (messageRecord.isMmsNotification()) {
Log.i(TAG, "Canceling MMS attachments download");
jobManager.cancel("mms-operation");
} else {
Log.i(TAG, "Canceling push attachment downloads for " + slides.size() + " items");
for (Slide slide : slides) {
final String queue = AttachmentDownloadJob.constructQueueString(((DatabaseAttachment) slide.asAttachment()).getAttachmentId());
jobManager.cancelAllInQueue(queue);
}
}
}
}
private class PlayVideoClickListener implements SlideClickListener {
private static final float MINIMUM_DOWNLOADED_THRESHOLD = 0.05f;
private View parentView;
private Slide activeSlide;
private class ProgressWheelClickListener implements SlideClickListener {
@Override
public void onClick(View v, Slide slide) {
if (messageRecord.isOutgoing()) {
Log.d(TAG, "Video player button for outgoing slide clicked.");
return;
}
if (MediaUtil.isInstantVideoSupported(slide)) {
final DatabaseAttachment databaseAttachment = (DatabaseAttachment) slide.asAttachment();
if (databaseAttachment.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_STARTED) {
final AttachmentId attachmentId = databaseAttachment.getAttachmentId();
final JobManager jobManager = ApplicationDependencies.getJobManager();
final String queue = AttachmentDownloadJob.constructQueueString(attachmentId);
setup(v, slide);
jobManager.add(new AttachmentDownloadJob(messageRecord.getId(),
attachmentId,
true));
jobManager.addListener(queue, (job, jobState) -> {
if (jobState.isComplete()) {
cleanup();
}
});
} else {
launchMediaPreview(v, slide);
cleanup();
}
final boolean isIncremental = slide.asAttachment().getIncrementalDigest() != null;
final boolean contentTypeSupported = MediaUtil.isVideoType(slide.getContentType());
if (FeatureFlags.instantVideoPlayback() && isIncremental && contentTypeSupported) {
launchMediaPreview(v, slide);
} else {
Log.d(TAG, "Non-eligible slide clicked.");
Log.d(TAG, "Non-eligible slide clicked: " + "\tisIncremental: " + isIncremental + "\tcontentTypeSupported: " + contentTypeSupported);
}
}
private void setup(View v, Slide slide) {
parentView = v;
activeSlide = slide;
if (!EventBus.getDefault().isRegistered(this)) EventBus.getDefault().register(this);
}
private void cleanup() {
parentView = null;
activeSlide = null;
if (EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().unregister(this);
}
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventAsync(PartProgressEvent event) {
float progressPercent = ((float) event.progress) / event.total;
final View currentParentView = parentView;
final Slide currentActiveSlide = activeSlide;
if (progressPercent >= MINIMUM_DOWNLOADED_THRESHOLD && currentParentView != null && currentActiveSlide != null) {
cleanup();
launchMediaPreview(currentParentView, currentActiveSlide);
}
}
}
private class SlideClickPassthroughListener implements SlideClickListener {

View File

@@ -1,19 +1,19 @@
package org.thoughtcrime.securesms.conversation
sealed class ConversationItemDisplayMode(val scheduleMessageMode: Boolean = false) {
enum class ConversationItemDisplayMode {
/** Normal rendering, used for normal bubbles in the conversation view */
object Standard : ConversationItemDisplayMode()
STANDARD,
/** Smaller bubbles, often trimming text and shrinking images. Used for quote threads. */
class Condensed(scheduleMessageMode: Boolean) : ConversationItemDisplayMode(scheduleMessageMode)
CONDENSED,
/** Smaller bubbles, always singular bubbles, with a footer. Used for edit message history. */
object EditHistory : ConversationItemDisplayMode()
EDIT_HISTORY,
/** Less length restrictions. Used to show more info in message details. */
object Detailed : ConversationItemDisplayMode()
DETAILED;
fun displayWallpaper(): Boolean {
return this == Standard || this == Detailed
return this == STANDARD || this == DETAILED
}
}

View File

@@ -12,7 +12,6 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
import org.thoughtcrime.securesms.conversation.v2.computed.FormattedDate;
import org.thoughtcrime.securesms.database.BodyRangeUtil;
import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.NoSuchMessageException;
@@ -46,7 +45,7 @@ public class ConversationMessage {
@NonNull private final Recipient threadRecipient;
private final boolean hasBeenQuoted;
@Nullable private final MessageRecord originalMessage;
@NonNull private final ComputedProperties computedProperties;
@NonNull private final String formattedDate;
private ConversationMessage(@NonNull MessageRecord messageRecord,
@Nullable CharSequence body,
@@ -55,15 +54,15 @@ public class ConversationMessage {
@Nullable MessageStyler.Result styleResult,
@NonNull Recipient threadRecipient,
@Nullable MessageRecord originalMessage,
@NonNull ComputedProperties computedProperties)
@NonNull String formattedDate)
{
this.messageRecord = messageRecord;
this.hasBeenQuoted = hasBeenQuoted;
this.mentions = mentions != null ? mentions : Collections.emptyList();
this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none();
this.threadRecipient = threadRecipient;
this.originalMessage = originalMessage;
this.computedProperties = computedProperties;
this.messageRecord = messageRecord;
this.hasBeenQuoted = hasBeenQuoted;
this.mentions = mentions != null ? mentions : Collections.emptyList();
this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none();
this.threadRecipient = threadRecipient;
this.originalMessage = originalMessage;
this.formattedDate = formattedDate;
if (body != null) {
this.body = SpannableString.valueOf(body);
@@ -96,8 +95,9 @@ public class ConversationMessage {
return hasBeenQuoted;
}
public @NonNull ComputedProperties getComputedProperties() {
return computedProperties;
@NonNull
public String getFormattedDate() {
return formattedDate;
}
@Override
@@ -160,27 +160,6 @@ public class ConversationMessage {
return threadRecipient;
}
public static @NonNull FormattedDate getFormattedDate(@NonNull Context context, @NonNull MessageRecord messageRecord) {
return MessageRecordUtil.isScheduled(messageRecord) ? new FormattedDate(false, DateUtils.getOnlyTimeString(context, Locale.getDefault(), ((MediaMmsMessageRecord) messageRecord).getScheduledDate()))
: DateUtils.getDatelessRelativeTimeSpanFormattedDate(context, Locale.getDefault(), messageRecord.getTimestamp());
}
public static class ComputedProperties {
private @NonNull FormattedDate formattedDate;
ComputedProperties(@NonNull FormattedDate formattedDate) {
this.formattedDate = formattedDate;
}
public synchronized FormattedDate getFormattedDate() {
return formattedDate;
}
public synchronized void setFormattedDate(@NonNull FormattedDate formattedDate) {
this.formattedDate = formattedDate;
}
}
/**
* Factory providing multiple ways of creating {@link ConversationMessage}s.
*/
@@ -225,8 +204,8 @@ public class ConversationMessage {
}
}
FormattedDate formattedDate = getFormattedDate(context, messageRecord);
String formattedDate = MessageRecordUtil.isScheduled(messageRecord) ? DateUtils.getOnlyTimeString(context, Locale.getDefault(), ((MediaMmsMessageRecord) messageRecord).getScheduledDate())
: DateUtils.getDatelessRelativeTimeSpanString(context, Locale.getDefault(), messageRecord.getTimestamp());
return new ConversationMessage(messageRecord,
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
mentionsUpdate != null ? mentionsUpdate.getMentions() : null,
@@ -234,7 +213,7 @@ public class ConversationMessage {
styleResult,
threadRecipient,
originalMessage,
new ComputedProperties(formattedDate));
formattedDate);
}
/**

View File

@@ -454,7 +454,7 @@ public final class ConversationUpdateItem extends FrameLayout
if (Util.hasItems(acis)) {
if (acis.contains(SignalStore.account().requireAci())) {
text = R.string.ConversationUpdateItem_return_to_call;
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).isCallFull) {
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) {
text = R.string.ConversationUpdateItem_call_is_full;
} else {
text = R.string.ConversationUpdateItem_join_call;
@@ -712,7 +712,9 @@ public final class ConversationUpdateItem extends FrameLayout
return;
}
IdentityUtil.getRemoteIdentityKey(getContext(), messageRecord.getToRecipient()).addListener(new ListenableFuture.Listener<>() {
final Recipient sender = ConversationUpdateItem.this.senderObserver.getObservedRecipient();
IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
@Override
public void onSuccess(Optional<IdentityRecord> result) {
if (result.isPresent()) {

View File

@@ -63,13 +63,13 @@ object MessageStyler {
var bottomButton: BodyRange.Button? = null
messageRanges
.ranges
.rangesList
.filter { r -> r.start >= 0 && r.start < span.length && r.start + r.length >= 0 }
.forEach { range ->
val start = range.start
val end = (range.start + range.length).coerceAtMost(span.length)
if (range.style != null) {
if (range.hasStyle()) {
val styleSpan: Any? = when (range.style) {
BodyRange.Style.BOLD -> boldStyle()
BodyRange.Style.ITALIC -> italicStyle()
@@ -90,10 +90,10 @@ object MessageStyler {
span.setSpan(styleSpan, start, end, SPAN_FLAGS)
appliedStyle = true
}
} else if (range.link != null) {
} else if (range.hasLink() && range.link != null) {
span.setSpan(PlaceholderURLSpan(range.link), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
hasLinks = true
} else if (range.button != null) {
} else if (range.hasButton() && range.button != null) {
bottomButton = range.button
}
}
@@ -245,7 +245,7 @@ object MessageStyler {
}
if (spanLength > 0 && style != null) {
BodyRange(start = spanStart, length = spanLength, style = style)
BodyRange.newBuilder().setStart(spanStart).setLength(spanLength).setStyle(style).build()
} else {
null
}
@@ -255,7 +255,7 @@ object MessageStyler {
}
return if (bodyRanges.isNotEmpty()) {
BodyRangeList(ranges = bodyRanges)
BodyRangeList.newBuilder().addAllRanges(bodyRanges).build()
} else {
null
}

View File

@@ -93,7 +93,8 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
val colorizer = Colorizer()
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient.hasWallpaper(), colorizer).apply {
setCondensedMode(ConversationItemDisplayMode.Condensed(scheduleMessageMode = true))
setCondensedMode(ConversationItemDisplayMode.CONDENSED)
setScheduledMessagesMode(true)
}
val list: RecyclerView = view.findViewById<RecyclerView>(R.id.scheduled_list).apply {

View File

@@ -73,20 +73,20 @@ class ChatColors(
}
fun serialize(): ChatColor {
val builder: ChatColor.Builder = ChatColor.Builder()
val builder: ChatColor.Builder = ChatColor.newBuilder()
if (linearGradient != null) {
val gradientBuilder = ChatColor.LinearGradient.Builder()
val gradientBuilder = ChatColor.LinearGradient.newBuilder()
gradientBuilder.rotation = linearGradient.degrees
gradientBuilder.colors = linearGradient.colors.toList()
gradientBuilder.positions = linearGradient.positions.toList()
linearGradient.colors.forEach { gradientBuilder.addColors(it) }
linearGradient.positions.forEach { gradientBuilder.addPositions(it) }
builder.linearGradient(gradientBuilder.build())
builder.setLinearGradient(gradientBuilder)
}
if (singleColor != null) {
builder.singleColor(ChatColor.SingleColor.Builder().color(singleColor).build())
builder.setSingleColor(ChatColor.SingleColor.newBuilder().setColor(singleColor))
}
return builder.build()
@@ -142,18 +142,18 @@ class ChatColors(
companion object {
@JvmStatic
fun forChatColor(id: Id, chatColor: ChatColor): ChatColors {
assert((chatColor.singleColor != null) xor (chatColor.linearGradient != null))
assert(chatColor.hasSingleColor() xor chatColor.hasLinearGradient())
return if (chatColor.linearGradient != null) {
return if (chatColor.hasLinearGradient()) {
val linearGradient = LinearGradient(
chatColor.linearGradient.rotation,
chatColor.linearGradient.colors.toIntArray(),
chatColor.linearGradient.positions.toFloatArray()
chatColor.linearGradient.colorsList.toIntArray(),
chatColor.linearGradient.positionsList.toFloatArray()
)
forGradient(id, linearGradient)
} else {
val singleColor = chatColor.singleColor!!.color
val singleColor = chatColor.singleColor.color
forColor(id, singleColor)
}

View File

@@ -191,7 +191,7 @@ class DraftRepository(
var updatedText: Spannable? = null
if (textDraft != null && bodyRangesDraft != null) {
val bodyRanges: BodyRangeList = BodyRangeList.ADAPTER.decode(Base64.decodeOrThrow(bodyRangesDraft.value))
val bodyRanges: BodyRangeList = BodyRangeList.parseFrom(Base64.decodeOrThrow(bodyRangesDraft.value))
val mentions: List<Mention> = MentionUtil.bodyRangeListToMentions(bodyRanges)
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, textDraft.value, mentions)

View File

@@ -62,7 +62,7 @@ class DraftViewModel @JvmOverloads constructor(
} else if (mentionRanges == null) {
styleBodyRanges
} else {
styleBodyRanges.newBuilder().apply { ranges += mentionRanges.ranges }.build()
styleBodyRanges.toBuilder().addAllRanges(mentionRanges.rangesList).build()
}
saveDrafts(it.copy(textDraft = text.toTextDraft(), bodyRangesDraft = bodyRanges?.toDraft(), messageEditDraft = Draft(Draft.MESSAGE_EDIT, messageId.serialize())))
@@ -84,7 +84,7 @@ class DraftViewModel @JvmOverloads constructor(
} else if (mentionRanges == null) {
styleBodyRanges
} else {
styleBodyRanges.newBuilder().apply { ranges += mentionRanges.ranges }.build()
styleBodyRanges.toBuilder().addAllRanges(mentionRanges.rangesList).build()
}
saveDrafts(it.copy(textDraft = text.toTextDraft(), bodyRangesDraft = bodyRanges?.toDraft()))
@@ -148,5 +148,5 @@ private fun String.toTextDraft(): Draft? {
}
private fun BodyRangeList.toDraft(): Draft {
return Draft(Draft.BODY_RANGES, Base64.encodeBytes(encode()))
return Draft(Draft.BODY_RANGES, Base64.encodeBytes(toByteArray()))
}

View File

@@ -65,6 +65,7 @@ class ConversationItemAnimator(
private fun animateSlide(viewHolder: RecyclerView.ViewHolder, preLayoutInfo: ItemHolderInfo?, postLayoutInfo: ItemHolderInfo): Boolean {
if (isInMultiSelectMode() || !shouldPlayMessageAnimations()) {
Log.v(TAG, "Dropping slide animation: (${isInMultiSelectMode()}, ${shouldPlayMessageAnimations()}) :: ${viewHolder.absoluteAdapterPosition}")
dispatchAnimationFinished(viewHolder)
return false
}
@@ -104,6 +105,7 @@ class ConversationItemAnimator(
animateSlide(viewHolder, preLayoutInfo, postLayoutInfo)
}
} else {
Log.v(TAG, "Dropping persistence animation: (${isInMultiSelectMode()}, ${shouldPlayMessageAnimations()}, ${isParentFilled()}) :: ${viewHolder.absoluteAdapterPosition}")
dispatchAnimationFinished(viewHolder)
false
}

View File

@@ -27,7 +27,6 @@ import androidx.core.view.children
import androidx.core.view.forEach
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.SimpleColorFilter
import com.google.android.material.animation.ArgbEvaluatorCompat
@@ -125,15 +124,7 @@ class MultiselectItemDecoration(
}
private fun getCurrentSelection(parent: RecyclerView): Set<MultiselectPart> {
return parent.findAdapterBridge().selectedItems
}
private fun RecyclerView.findAdapterBridge(): ConversationAdapterBridge {
return when (val parentAdapter = adapter!!) {
is ConversationAdapterBridge -> parentAdapter
is ConcatAdapter -> (parentAdapter.adapters[1] as ConversationAdapterBridge)
else -> error("Unexpected adapter configuration")
}
return (parent.adapter as ConversationAdapterBridge).selectedItems
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
@@ -165,14 +156,14 @@ class MultiselectItemDecoration(
outRect.setEmpty()
updateChildOffsets(parent, view)
consumePulseRequest(parent.findAdapterBridge())
consumePulseRequest(parent.adapter as ConversationAdapterBridge)
}
/**
* Draws the background shade.
*/
override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val adapter = parent.findAdapterBridge()
val adapter = parent.adapter as ConversationAdapterBridge
if (adapter.selectedItems.isEmpty()) {
drawFocusShadeUnderIfNecessary(canvas, parent)
@@ -240,7 +231,7 @@ class MultiselectItemDecoration(
* Draws the selected check or empty circle.
*/
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val adapter = parent.findAdapterBridge()
val adapter = parent.adapter as ConversationAdapterBridge
if (adapter.selectedItems.isEmpty()) {
drawFocusShadeOverIfNecessary(canvas, parent)
}
@@ -356,7 +347,7 @@ class MultiselectItemDecoration(
* called in getItemOffsets to ensure the gutter goes away when multiselect mode ends.
*/
private fun updateChildOffsets(parent: RecyclerView, child: View) {
val adapter = parent.findAdapterBridge()
val adapter = parent.adapter as ConversationAdapterBridge
val isLtr = ViewUtil.isLtr(child)
val multiselectable: Multiselectable = resolveMultiselectable(parent, child) ?: return
@@ -575,13 +566,19 @@ class MultiselectItemDecoration(
}
private fun RecyclerView.getInteractableChildren(): Sequence<InteractiveConversationElement> {
return children.map { getChildViewHolder(it) }.filterIsInstance<InteractiveConversationElement>() + children.filterIsInstance<InteractiveConversationElement>()
return if (FeatureFlags.useTextOnlyConversationItemV2()) {
children.map { getChildViewHolder(it) }.filterIsInstance<InteractiveConversationElement>()
} else {
children.filterIsInstance<InteractiveConversationElement>()
}
}
private fun resolveMultiselectable(parent: RecyclerView, child: View): Multiselectable? {
val multiselectable = parent.getChildViewHolder(child) as? Multiselectable
return multiselectable ?: child as? Multiselectable
return if (FeatureFlags.useTextOnlyConversationItemV2()) {
parent.getChildViewHolder(child) as? Multiselectable
} else {
child as? Multiselectable
}
}
private class PulseAnimator(pulseColor: Int) {

View File

@@ -8,7 +8,6 @@ import android.view.ViewGroup
import androidx.fragment.app.setFragmentResult
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.stories.Stories
@@ -20,17 +19,11 @@ class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragmen
private var callback: Callback? = null
companion object {
private val TAG = Log.tag(MultiselectForwardBottomSheet::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.multiselect_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Log.d(TAG, "onViewCreated()")
callback = findListener<Callback>()
if (savedInstanceState == null) {
@@ -60,22 +53,18 @@ class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragmen
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
Log.d(TAG, "onDismiss()")
callback?.onDismissForwardSheet()
}
override fun onFinishForwardAction() {
Log.d(TAG, "onFinishForwardAction()")
callback?.onFinishForwardAction()
}
override fun exitFlow() {
Log.d(TAG, "exitFlow()")
dismissAllowingStateLoss()
}
override fun onSearchInputFocused() {
Log.d(TAG, "onSearchInputFocused()")
(requireDialog() as BottomSheetDialog).behavior.state = BottomSheetBehavior.STATE_EXPANDED
}

View File

@@ -117,8 +117,6 @@ class MultiselectForwardFragment :
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Log.d(TAG, "onViewCreated()")
view.minimumHeight = resources.displayMetrics.heightPixels
contactSearchRecycler = view.findViewById(R.id.contact_selection_list)
@@ -135,9 +133,7 @@ class MultiselectForwardFragment :
this::getConfiguration,
object : ContactSearchMediator.SimpleCallbacks() {
override fun onBeforeContactsSelected(view: View?, contactSearchKeys: Set<ContactSearchKey>): Set<ContactSearchKey> {
val filtered: Set<ContactSearchKey> = filterContacts(view, contactSearchKeys)
Log.d(TAG, "onBeforeContactsSelected() Attempting to select: ${contactSearchKeys.map { it.toString() }}, Filtered selection: ${filtered.map { it.toString() } }")
return filtered
return filterContacts(view, contactSearchKeys)
}
}
)
@@ -228,6 +224,7 @@ class MultiselectForwardFragment :
disposables += contactSearchMediator
.getErrorEvents()
.subscribe {
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
val message: Int = when (it) {
ContactSearchError.CONTACT_NOT_SELECTABLE -> R.string.MultiselectForwardFragment__only_admins_can_send_messages_to_this_group
ContactSearchError.RECOMMENDED_LIMIT_REACHED -> R.string.ContactSelectionListFragment_recommended_member_limit_reached
@@ -238,8 +235,6 @@ class MultiselectForwardFragment :
}
viewModel.state.observe(viewLifecycleOwner) {
Log.d(TAG, "State change: ${it.stage.javaClass.simpleName}")
when (it.stage) {
MultiselectForwardState.Stage.Selection -> {}
MultiselectForwardState.Stage.FirstConfirmation -> displayFirstSendConfirmation()
@@ -277,8 +272,6 @@ class MultiselectForwardFragment :
override fun onResume() {
super.onResume()
Log.d(TAG, "onViewCreated()")
val now = System.currentTimeMillis()
val expiringMessages = args.multiShareArgs.filter { it.expiresAt > 0L }
val firstToExpire = expiringMessages.minByOrNull { it.expiresAt }
@@ -338,8 +331,6 @@ class MultiselectForwardFragment :
}
private fun dismissWithSuccess(@PluralsRes toastTextResId: Int) {
Log.d(TAG, "dismissWithSuccess() Selected: ${contactSearchMediator.getSelectedContacts().map { it.toString() }}")
requireListener<Callback>().setResult(
Bundle().apply {
putBoolean(RESULT_SENT, true)
@@ -350,7 +341,7 @@ class MultiselectForwardFragment :
}
private fun dismissAndShowToast(@PluralsRes toastTextResId: Int) {
Log.d(TAG, "dismissAndShowToast() Selected: ${contactSearchMediator.getSelectedContacts().map { it.toString() }}")
Log.d(TAG, "dismissAndShowToast")
val argCount = getMessageCount()
@@ -372,7 +363,7 @@ class MultiselectForwardFragment :
}
private fun dismissWithSelection(selectedContacts: Set<ContactSearchKey>) {
Log.d(TAG, "dismissWithSelection() Selected: ${selectedContacts.map { it.toString() }}")
Log.d(TAG, "dismissWithSelection")
callback.onFinishForwardAction()
dismissibleDialog?.dismiss()

View File

@@ -74,7 +74,7 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
val colorizer = Colorizer()
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient.hasWallpaper(), colorizer).apply {
setCondensedMode(ConversationItemDisplayMode.Condensed(scheduleMessageMode = false))
setCondensedMode(ConversationItemDisplayMode.CONDENSED)
}
val list: RecyclerView = view.findViewById<RecyclerView>(R.id.quotes_list).apply {

View File

@@ -91,7 +91,7 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() {
conversationRecipient.hasWallpaper(),
colorizer
).apply {
setCondensedMode(ConversationItemDisplayMode.EditHistory)
setCondensedMode(ConversationItemDisplayMode.EDIT_HISTORY)
}
binding.editHistoryList.apply {

View File

@@ -187,7 +187,7 @@ class ConversationAdapterV2(
}
override val displayMode: ConversationItemDisplayMode
get() = condensedMode ?: ConversationItemDisplayMode.Standard
get() = condensedMode ?: ConversationItemDisplayMode.STANDARD
override fun onStartExpirationTimeout(messageRecord: MessageRecord) {
startExpirationTimeout(messageRecord)
@@ -484,7 +484,7 @@ class ConversationAdapterV2(
get() = getConversationMessage(bindingAdapterPosition - 1)?.messageRecord.toOptional()
protected val displayMode: ConversationItemDisplayMode
get() = condensedMode ?: ConversationItemDisplayMode.Standard
get() = condensedMode ?: ConversationItemDisplayMode.STANDARD
override val conversationMessage: ConversationMessage
get() = bindable.conversationMessage

View File

@@ -62,7 +62,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.ConversationLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -184,8 +183,6 @@ import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChanged
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryReplacement
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsControllerV2
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModelV2
import org.thoughtcrime.securesms.conversation.v2.computed.ConversationMessageComputeWorkers
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel
import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable
@@ -469,7 +466,6 @@ class ConversationFragment :
private lateinit var conversationActivityResultContracts: ConversationActivityResultContracts
private lateinit var scrollToPositionDelegate: ScrollToPositionDelegate
private lateinit var adapter: ConversationAdapterV2
private lateinit var typingIndicatorAdapter: ConversationTypingIndicatorAdapter
private lateinit var recyclerViewColorizer: RecyclerViewColorizer
private lateinit var attachmentManager: AttachmentManager
private lateinit var multiselectItemDecoration: MultiselectItemDecoration
@@ -477,6 +473,7 @@ class ConversationFragment :
private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration
private lateinit var conversationItemDecorations: ConversationItemDecorations
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration
private lateinit var backPressedCallback: BackPressedDelegate
private var animationsAllowed = false
@@ -543,8 +540,6 @@ class ConversationFragment :
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.toolbar.isBackInvokedCallbackEnabled = false
disposables.bindTo(viewLifecycleOwner)
FullscreenHelper(requireActivity()).showSystemUI()
@@ -1043,13 +1038,7 @@ class ConversationFragment :
getVoiceNoteMediaController().voiceNotePlaybackState.observe(viewLifecycleOwner, inputPanel.playbackStateObserver)
val conversationUpdateTick = ConversationUpdateTick {
disposables += ConversationMessageComputeWorkers.recomputeFormattedDate(
requireContext(),
adapter.currentList.filterIsInstance<ConversationMessageElement>()
).observeOn(AndroidSchedulers.mainThread()).subscribeBy { adapter.updateTimestamps() }
}
val conversationUpdateTick = ConversationUpdateTick { adapter.updateTimestamps() }
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
if (args.conversationScreenType.isInPopup) {
@@ -1087,24 +1076,18 @@ class ConversationFragment :
}
private fun presentTypingIndicator() {
typingIndicatorAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0 && itemCount == 1 && layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
scrollToPositionDelegate.resetScrollPosition()
}
}
})
typingIndicatorDecoration = TypingIndicatorDecoration(requireContext(), binding.conversationItemRecycler)
binding.conversationItemRecycler.addItemDecoration(typingIndicatorDecoration)
ApplicationDependencies.getTypingStatusRepository().getTypists(args.threadId).observe(viewLifecycleOwner) {
val recipient = viewModel.recipientSnapshot ?: return@observe
typingIndicatorAdapter.setState(
ConversationTypingIndicatorAdapter.State(
typists = it.typists,
isGroupThread = recipient.isGroup,
hasWallpaper = recipient.hasWallpaper(),
isReplacedByIncomingMessage = it.isReplacedByIncomingMessage
)
typingIndicatorDecoration.setTypists(
GlideApp.with(this),
it.typists,
recipient.isGroup,
recipient.hasWallpaper(),
it.isReplacedByIncomingMessage
)
}
}
@@ -1538,8 +1521,6 @@ class ConversationFragment :
startExpirationTimeout = viewModel::startExpirationTimeout
)
typingIndicatorAdapter = ConversationTypingIndicatorAdapter(GlideApp.with(this))
scrollToPositionDelegate = ScrollToPositionDelegate(
recyclerView = binding.conversationItemRecycler,
canJumpToPosition = adapter::canJumpToPosition
@@ -1550,7 +1531,7 @@ class ConversationFragment :
recyclerViewColorizer = RecyclerViewColorizer(binding.conversationItemRecycler)
recyclerViewColorizer.setChatColors(args.chatColors)
binding.conversationItemRecycler.adapter = ConcatAdapter(typingIndicatorAdapter, adapter)
binding.conversationItemRecycler.adapter = adapter
multiselectItemDecoration = MultiselectItemDecoration(
requireContext()
) { viewModel.wallpaperSnapshot }
@@ -1796,7 +1777,7 @@ class ConversationFragment :
return
}
if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && bodyRanges != null && bodyRanges.ranges.isNotEmpty()) {
if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && bodyRanges != null && bodyRanges.rangesCount > 0) {
Dialogs.showFormattedTextDialog(requireContext()) {
sendMessage(body, mentions, bodyRanges, messageToEdit, quote, scheduledDate, slideDeck, contacts, clearCompose, linkPreviews, preUploadResults, bypassPreSendSafetyNumberCheck, isViewOnce, afterSendComplete)
}
@@ -3824,9 +3805,7 @@ class ConversationFragment :
}
override fun onRecorderCanceled(byUser: Boolean) {
if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
updateToggleButtonState()
}
updateToggleButtonState()
voiceMessageRecordingDelegate.onRecorderCanceled(byUser)
}
@@ -3992,10 +3971,6 @@ class ConversationFragment :
override fun onKeyboardHidden() {
closeEmojiSearch()
if (searchMenuItem?.isActionViewExpanded == true && searchMenuItem?.actionView?.hasFocus() == true) {
searchMenuItem?.actionView?.clearFocus()
}
}
}

View File

@@ -55,14 +55,7 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val viewHolder = parent.getChildViewHolder(view)
if (viewHolder is ConversationTypingIndicatorAdapter.ViewHolder) {
outRect.set(0, 0, 0, 0)
return
}
val position = viewHolder.bindingAdapterPosition
val position = parent.getChildAdapterPosition(view)
val unreadHeight = if (isFirstUnread(position)) {
getUnreadViewHolder(parent).height
@@ -83,15 +76,9 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
val count = parent.childCount
for (layoutPosition in 0 until count) {
val child = parent.getChildAt(count - 1 - layoutPosition)
val viewHolder = parent.getChildViewHolder(child)
val position = parent.getChildAdapterPosition(child)
if (viewHolder is ConversationTypingIndicatorAdapter.ViewHolder) {
continue
}
val bindingAdapterPosition = viewHolder.bindingAdapterPosition
val unreadOffset = if (isFirstUnread(bindingAdapterPosition)) {
val unreadOffset = if (isFirstUnread(position)) {
val unread = getUnreadViewHolder(parent)
unread.itemView.drawAsTopItemDecoration(c, parent, child)
unread.height
@@ -99,8 +86,8 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
0
}
if (hasHeader(bindingAdapterPosition)) {
val headerView = getHeader(parent, currentItems[bindingAdapterPosition] as ConversationMessageElement).itemView
if (hasHeader(position)) {
val headerView = getHeader(parent, currentItems[position] as ConversationMessageElement).itemView
headerView.drawAsTopItemDecoration(c, parent, child, unreadOffset)
}
}
@@ -149,18 +136,18 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
}
}
private fun isFirstUnread(bindingAdapterPosition: Int): Boolean {
private fun isFirstUnread(position: Int): Boolean {
val state = unreadState
return state is UnreadState.CompleteUnreadState &&
state.firstUnreadTimestamp != null &&
bindingAdapterPosition in currentItems.indices &&
(currentItems[bindingAdapterPosition] as? ConversationMessageElement)?.timestamp() == state.firstUnreadTimestamp
position in currentItems.indices &&
(currentItems[position] as? ConversationMessageElement)?.timestamp() == state.firstUnreadTimestamp
}
private fun hasHeader(bindingAdapterPosition: Int): Boolean {
val model = if (bindingAdapterPosition in currentItems.indices) {
currentItems[bindingAdapterPosition]
private fun hasHeader(position: Int): Boolean {
val model = if (position in currentItems.indices) {
currentItems[position]
} else {
null
}
@@ -169,7 +156,7 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch
return false
}
val previousPosition = bindingAdapterPosition + 1
val previousPosition = position + 1
val previousDay: Long
if (previousPosition in currentItems.indices) {
val previousModel = currentItems[previousPosition]

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ConversationTypingView
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
class ConversationTypingIndicatorAdapter(
private val glideRequests: GlideRequests
) : RecyclerView.Adapter<ConversationTypingIndicatorAdapter.ViewHolder>() {
private var state: State = State()
fun setState(state: State) {
val isInsert = this.state.typists.isEmpty() && state.typists.isNotEmpty()
val isRemoval = state.typists.isEmpty() && this.state.typists.isNotEmpty()
val isChange = state.typists.isNotEmpty() && this.state.typists.isNotEmpty()
this.state = state
when {
isInsert -> notifyItemInserted(0)
isRemoval -> notifyItemRemoved(0)
isChange -> notifyItemChanged(0)
else -> Unit
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.conversation_typing_view, parent, false) as ConversationTypingView)
}
override fun getItemCount(): Int = state.typists.isNotEmpty().toInt()
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(glideRequests, state)
}
class ViewHolder(private val conversationTypingView: ConversationTypingView) : RecyclerView.ViewHolder(conversationTypingView) {
fun bind(
glideRequests: GlideRequests,
state: State
) {
conversationTypingView.setTypists(
glideRequests,
state.typists,
state.isGroupThread,
state.hasWallpaper
)
}
}
data class State(
val typists: List<Recipient> = emptyList(),
val hasWallpaper: Boolean = false,
val isGroupThread: Boolean = false,
val isReplacedByIncomingMessage: Boolean = false // TODO
)
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.View.MeasureSpec
import androidx.core.graphics.withTranslation
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ConversationTypingView
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
/**
* Displays a typing indicator as a part of the very last (first) item in the adapter.
*/
class TypingIndicatorDecoration(
private val context: Context,
private val rootView: RecyclerView
) : ItemDecoration() {
companion object {
private val TAG = Log.tag(TypingIndicatorDecoration::class.java)
}
private val typingView: ConversationTypingView by lazy(LazyThreadSafetyMode.NONE) {
LayoutInflater.from(context).inflate(R.layout.conversation_typing_view, rootView, false) as ConversationTypingView
}
private var displayIndicator = false
private var animationFraction = 0f
private var offsetAnimator: ValueAnimator? = null
init {
rootView.addOnLayoutChangeListener { _, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
remeasureTypingView()
rootView.invalidateItemDecorations()
}
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (!displayIndicator && animationFraction == 0f) {
return outRect.set(0, 0, 0, 0)
}
if (parent.getChildAdapterPosition(view) == 0) {
remeasureTypingView()
outRect.set(0, 0, 0, (typingView.measuredHeight * animationFraction).toInt())
} else {
outRect.set(0, 0, 0, 0)
}
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
if (!displayIndicator && offsetAnimator?.isRunning != true) {
return
}
val firstChild = parent.children.firstOrNull() ?: return
if (parent.getChildAdapterPosition(firstChild) == 0) {
c.withTranslation(
x = firstChild.left.toFloat(),
y = firstChild.bottom.toFloat()
) {
typingView.draw(this)
}
if (typingView.isActive) {
rootView.post { rootView.invalidateItemDecorations() }
}
}
}
fun setTypists(
glideRequests: GlideRequests,
typists: List<Recipient>,
isGroupThread: Boolean,
hasWallpaper: Boolean,
isReplacedByIncomingMessage: Boolean
) {
Log.d(TAG, "setTypists: Updating typists: ${typists.size} $isGroupThread $hasWallpaper $isReplacedByIncomingMessage")
val isEdge = displayIndicator != typists.isNotEmpty()
displayIndicator = typists.isNotEmpty()
typingView.setTypists(
glideRequests,
typists,
isGroupThread,
hasWallpaper
)
remeasureTypingView()
rootView.invalidateItemDecorations()
if (isReplacedByIncomingMessage) {
offsetAnimator?.cancel()
animationFraction = 0f
} else if (isEdge) {
animateOffset()
}
}
private fun animateOffset() {
offsetAnimator?.cancel()
val (start, end) = if (displayIndicator) {
animationFraction to 1f
} else {
animationFraction to 0f
}
offsetAnimator = ValueAnimator.ofFloat(start, end).apply {
addUpdateListener {
animationFraction = it.animatedValue as Float
rootView.invalidateItemDecorations()
}
start()
}
}
private fun remeasureTypingView() {
with(typingView) {
measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
)
layout(
0,
0,
measuredWidth,
measuredHeight
)
}
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2.computed
import android.content.Context
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
/**
* Collection of workers to recompute computed ConversationMessage fields.
*/
object ConversationMessageComputeWorkers {
private val executor = SignalExecutors.newCachedSingleThreadExecutor("conversation-message-compute", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD)
fun recomputeFormattedDate(
context: Context,
items: List<ConversationMessageElement>
): Single<Boolean> {
return Single.fromCallable {
var hasUpdatedProperties = false
for (item in items) {
val oldDate = item.conversationMessage.computedProperties.formattedDate
if (oldDate.isRelative) {
val newDate = ConversationMessage.getFormattedDate(context, item.conversationMessage.messageRecord)
item.conversationMessage.computedProperties.formattedDate = newDate
hasUpdatedProperties = true
}
}
hasUpdatedProperties
}.subscribeOn(Schedulers.from(executor))
}
}

View File

@@ -1,11 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2.computed
data class FormattedDate(
val isRelative: Boolean,
val value: String
)

View File

@@ -35,11 +35,7 @@ class V2ConversationItemLayout @JvmOverloads constructor(
onMeasureListeners.forEach { it.onPreMeasure() }
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
var remeasure = false
onMeasureListeners.forEach {
remeasure = it.onPostMeasure() || remeasure
}
val remeasure = onMeasureListeners.map { it.onPostMeasure() }.any { it }
if (remeasure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}

View File

@@ -33,16 +33,16 @@ fun V2ConversationItemMediaIncomingBinding.bridge(): V2ConversationItemMediaBind
senderName = groupMessageSender,
senderPhoto = contactPhoto,
senderBadge = badge,
body = conversationItemBody,
bodyWrapper = conversationItemBodyWrapper,
reply = conversationItemReply,
reactions = conversationItemReactions,
deliveryStatus = null,
footerDate = conversationItemFooterDate,
footerExpiry = conversationItemExpirationTimer,
footerBackground = conversationItemFooterBackground,
alert = null,
footerSpace = null,
conversationItemBody = conversationItemBody,
conversationItemBodyWrapper = conversationItemBodyWrapper,
conversationItemReply = conversationItemReply,
conversationItemReactions = conversationItemReactions,
conversationItemDeliveryStatus = null,
conversationItemFooterDate = conversationItemFooterDate,
conversationItemFooterExpiry = conversationItemExpirationTimer,
conversationItemFooterBackground = conversationItemFooterBackground,
conversationItemAlert = null,
conversationItemFooterSpace = null,
isIncoming = true
)
@@ -63,16 +63,16 @@ fun V2ConversationItemMediaOutgoingBinding.bridge(): V2ConversationItemMediaBind
senderName = null,
senderPhoto = null,
senderBadge = null,
body = conversationItemBody,
bodyWrapper = conversationItemBodyWrapper,
reply = conversationItemReply,
reactions = conversationItemReactions,
deliveryStatus = conversationItemDeliveryStatus,
footerDate = conversationItemFooterDate,
footerExpiry = conversationItemExpirationTimer,
footerBackground = conversationItemFooterBackground,
alert = conversationItemAlert,
footerSpace = footerEndPad,
conversationItemBody = conversationItemBody,
conversationItemBodyWrapper = conversationItemBodyWrapper,
conversationItemReply = conversationItemReply,
conversationItemReactions = conversationItemReactions,
conversationItemDeliveryStatus = conversationItemDeliveryStatus,
conversationItemFooterDate = conversationItemFooterDate,
conversationItemFooterExpiry = conversationItemExpirationTimer,
conversationItemFooterBackground = conversationItemFooterBackground,
conversationItemAlert = conversationItemAlert,
conversationItemFooterSpace = footerEndPad,
isIncoming = false
)

View File

@@ -30,7 +30,7 @@ class V2ConversationItemMediaViewHolder<Model : MappingModel<Model>>(
) {
init {
binding.textBridge.bodyWrapper.clipToOutline = true
binding.textBridge.conversationItemBodyWrapper.clipToOutline = true
}
override fun bind(model: Model) {
@@ -57,7 +57,7 @@ class V2ConversationItemMediaViewHolder<Model : MappingModel<Model>>(
0
}
this.constrainMaxWidth(binding.textBridge.bodyWrapper.id, maxBodyWidth)
this.constrainMaxWidth(binding.textBridge.conversationItemBodyWrapper.id, maxBodyWidth)
}
}

View File

@@ -18,12 +18,12 @@ class V2ConversationItemSnapshotStrategy(
) : InteractiveConversationElement.SnapshotStrategy {
private val viewsToRestoreScale = listOfNotNull(
binding.bodyWrapper,
binding.footerBackground,
binding.footerDate,
binding.footerExpiry,
binding.deliveryStatus,
binding.reactions
binding.conversationItemBodyWrapper,
binding.conversationItemFooterBackground,
binding.conversationItemFooterDate,
binding.conversationItemFooterExpiry,
binding.conversationItemDeliveryStatus,
binding.conversationItemReactions
)
private val viewsToHide = listOfNotNull(
@@ -33,7 +33,7 @@ class V2ConversationItemSnapshotStrategy(
override val snapshotMetrics = InteractiveConversationElement.SnapshotMetrics(
snapshotOffset = 0f,
contextMenuPadding = binding.bodyWrapper.x
contextMenuPadding = binding.conversationItemBodyWrapper.x
)
override fun snapshot(canvas: Canvas) {

View File

@@ -31,16 +31,16 @@ data class V2ConversationItemTextOnlyBindingBridge(
val senderName: EmojiTextView?,
val senderPhoto: AvatarImageView?,
val senderBadge: BadgeImageView?,
val bodyWrapper: ViewGroup,
val body: EmojiTextView,
val reply: ShapeableImageView,
val reactions: ReactionsConversationView,
val deliveryStatus: DeliveryStatusView?,
val footerDate: TextView,
val footerExpiry: ExpirationTimerView,
val footerBackground: View,
val footerSpace: Space?,
val alert: AlertView?,
val conversationItemBodyWrapper: ViewGroup,
val conversationItemBody: EmojiTextView,
val conversationItemReply: ShapeableImageView,
val conversationItemReactions: ReactionsConversationView,
val conversationItemDeliveryStatus: DeliveryStatusView?,
val conversationItemFooterDate: TextView,
val conversationItemFooterExpiry: ExpirationTimerView,
val conversationItemFooterBackground: View,
val conversationItemFooterSpace: Space?,
val conversationItemAlert: AlertView?,
val isIncoming: Boolean
)
@@ -53,16 +53,16 @@ fun V2ConversationItemTextOnlyIncomingBinding.bridge(): V2ConversationItemTextOn
senderName = groupMessageSender,
senderPhoto = contactPhoto,
senderBadge = badge,
body = conversationItemBody,
bodyWrapper = conversationItemBodyWrapper,
reply = conversationItemReply,
reactions = conversationItemReactions,
deliveryStatus = null,
footerDate = conversationItemFooterDate,
footerExpiry = conversationItemExpirationTimer,
footerBackground = conversationItemFooterBackground,
alert = null,
footerSpace = footerEndPad,
conversationItemBody = conversationItemBody,
conversationItemBodyWrapper = conversationItemBodyWrapper,
conversationItemReply = conversationItemReply,
conversationItemReactions = conversationItemReactions,
conversationItemDeliveryStatus = null,
conversationItemFooterDate = conversationItemFooterDate,
conversationItemFooterExpiry = conversationItemExpirationTimer,
conversationItemFooterBackground = conversationItemFooterBackground,
conversationItemAlert = null,
conversationItemFooterSpace = null,
isIncoming = true
)
}
@@ -76,16 +76,16 @@ fun V2ConversationItemTextOnlyOutgoingBinding.bridge(): V2ConversationItemTextOn
senderName = null,
senderPhoto = null,
senderBadge = null,
body = conversationItemBody,
bodyWrapper = conversationItemBodyWrapper,
reply = conversationItemReply,
reactions = conversationItemReactions,
deliveryStatus = conversationItemDeliveryStatus,
footerDate = conversationItemFooterDate,
footerExpiry = conversationItemExpirationTimer,
footerBackground = conversationItemFooterBackground,
alert = conversationItemAlert,
footerSpace = footerEndPad,
conversationItemBody = conversationItemBody,
conversationItemBodyWrapper = conversationItemBodyWrapper,
conversationItemReply = conversationItemReply,
conversationItemReactions = conversationItemReactions,
conversationItemDeliveryStatus = conversationItemDeliveryStatus,
conversationItemFooterDate = conversationItemFooterDate,
conversationItemFooterExpiry = conversationItemExpirationTimer,
conversationItemFooterBackground = conversationItemFooterBackground,
conversationItemAlert = conversationItemAlert,
conversationItemFooterSpace = footerEndPad,
isIncoming = false
)
}

View File

@@ -6,7 +6,6 @@
package org.thoughtcrime.securesms.conversation.v2.items
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableStringBuilder
@@ -52,7 +51,6 @@ import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.VibrateUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.hasExtraText
import org.thoughtcrime.securesms.util.hasNoBubble
import org.thoughtcrime.securesms.util.isScheduled
import org.thoughtcrime.securesms.util.visible
import java.util.Locale
@@ -82,28 +80,26 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
override lateinit var conversationMessage: ConversationMessage
override val root: ViewGroup = binding.root
override val bubbleView: View = binding.bodyWrapper
override val bubbleView: View = binding.conversationItemBodyWrapper
override val bubbleViews: List<View> = listOfNotNull(
binding.bodyWrapper,
binding.footerDate,
binding.footerExpiry,
binding.deliveryStatus,
binding.footerBackground
binding.conversationItemBodyWrapper,
binding.conversationItemFooterDate,
binding.conversationItemFooterExpiry,
binding.conversationItemDeliveryStatus,
binding.conversationItemFooterBackground
)
override val reactionsView: View = binding.reactions
override val reactionsView: View = binding.conversationItemReactions
override val quotedIndicatorView: View? = null
override val replyView: View = binding.reply
override val replyView: View = binding.conversationItemReply
override val contactPhotoHolderView: View? = binding.senderPhoto
override val badgeImageView: View? = binding.senderBadge
private var reactionMeasureListener: ReactionMeasureListener = ReactionMeasureListener()
private var dateString: String = ""
private val bodyBubbleDrawable = ChatColorsDrawable()
private val footerDrawable = ChatColorsDrawable()
private val senderDrawable = ChatColorsDrawable()
private val bodyBubbleLayoutTransition = BodyBubbleLayoutTransition()
protected lateinit var shape: V2ConversationItemShape.MessageShape
@@ -112,21 +108,21 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
override fun onPreMeasure() = Unit
override fun onPostMeasure(): Boolean {
val wrapperHeight = binding.conversationItemBodyWrapper.measuredHeight
val yTranslation = (wrapperHeight - 38.dp) / 2f
binding.conversationItemReply.translationY = -yTranslation
return false
}
}
init {
binding.root.addOnMeasureListener(footerDelegate)
binding.bodyWrapper.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
val wrapperHeight = bottom - top
val yTranslation = (wrapperHeight - 38.dp) / 2f
binding.reply.translationY = -yTranslation
}
binding.root.addOnMeasureListener(replyDelegate)
binding.root.onDispatchTouchEventListener = dispatchTouchEventListener
binding.reactions.setOnClickListener {
binding.conversationItemReactions.setOnClickListener {
conversationContext.clickListener
.onReactionClicked(
Multiselect.getParts(conversationMessage).asSingle().singlePart,
@@ -146,23 +142,23 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
}
val passthroughClickListener = PassthroughClickListener()
binding.body.setOnClickListener(passthroughClickListener)
binding.body.setOnLongClickListener(passthroughClickListener)
binding.conversationItemBody.setOnClickListener(passthroughClickListener)
binding.conversationItemBody.setOnLongClickListener(passthroughClickListener)
binding.body.isFocusable = false
binding.body.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().messageFontSize.toFloat())
binding.body.movementMethod = LongClickMovementMethod.getInstance(context)
binding.conversationItemBody.isFocusable = false
binding.conversationItemBody.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().messageFontSize.toFloat())
binding.conversationItemBody.movementMethod = LongClickMovementMethod.getInstance(context)
if (binding.isIncoming) {
binding.body.setMentionBackgroundTint(ContextCompat.getColor(context, if (ThemeUtil.isDarkTheme(context)) R.color.core_grey_60 else R.color.core_grey_20))
binding.conversationItemBody.setMentionBackgroundTint(ContextCompat.getColor(context, if (ThemeUtil.isDarkTheme(context)) R.color.core_grey_60 else R.color.core_grey_20))
} else {
binding.body.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25))
binding.conversationItemBody.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25))
}
binding.bodyWrapper.background = bodyBubbleDrawable
binding.bodyWrapper.layoutTransition = bodyBubbleLayoutTransition
binding.conversationItemBodyWrapper.background = bodyBubbleDrawable
binding.conversationItemBodyWrapper.layoutTransition = bodyBubbleLayoutTransition
binding.footerBackground.background = footerDrawable
binding.conversationItemFooterBackground.background = footerDrawable
}
override fun invalidateChatColorsDrawable(coordinateRoot: ViewGroup) {
@@ -173,7 +169,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
override fun bind(model: Model) {
var hasProcessedSupportedPayload = false
binding.bodyWrapper.layoutTransition = if (conversationContext.isParentInScroll) {
binding.conversationItemBodyWrapper.layoutTransition = if (conversationContext.isParentInScroll) {
null
} else {
bodyBubbleLayoutTransition
@@ -198,9 +194,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
)
if (ConversationAdapterBridge.PAYLOAD_TIMESTAMP in payload) {
if (conversationMessage.computedProperties.formattedDate.value != dateString) {
presentDate()
}
presentDate()
hasProcessedSupportedPayload = true
}
@@ -223,15 +217,13 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
presentDeliveryStatus()
presentFooterBackground()
presentFooterExpiry()
presentFooterEndPadding()
presentAlert()
presentSender()
presentSenderNameColor()
presentSenderNameBackground()
presentReactions()
bodyBubbleDrawable.setChatColors(
if (binding.body.isJumbomoji) {
if (binding.conversationItemBody.isJumbomoji) {
transparentChatColors
} else if (binding.isIncoming) {
ChatColors.forColor(ChatColors.Id.NotSet, themeDelegate.getBodyBubbleColor(conversationMessage))
@@ -241,7 +233,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
shapeDelegate.corners
)
binding.reply.setBackgroundColor(themeDelegate.getReplyIconBackgroundColor())
binding.conversationItemReply.setBackgroundColor(themeDelegate.getReplyIconBackgroundColor())
itemView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = shape.topPadding.toInt()
@@ -265,9 +257,9 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
projections.add(
Projection.relativeToParent(
coordinateRoot,
binding.bodyWrapper,
binding.conversationItemBodyWrapper,
shapeDelegate.corners
).translateX(binding.bodyWrapper.translationX).translateY(root.translationY)
).translateX(binding.conversationItemBodyWrapper.translationX).translateY(root.translationY)
)
return projections
@@ -305,7 +297,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
} else if (conversationMessage.threadRecipient.isGroup) {
binding.senderPhoto
} else {
binding.bodyWrapper
binding.conversationItemBodyWrapper
}
}
@@ -317,11 +309,11 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
override fun getGiphyMp4PlayableProjection(coordinateRoot: ViewGroup): Projection {
return Projection.relativeToParent(
coordinateRoot,
binding.bodyWrapper,
binding.conversationItemBodyWrapper,
shapeDelegate.corners
)
.translateY(root.translationY)
.translateX(binding.bodyWrapper.translationX)
.translateX(binding.conversationItemBodyWrapper.translationX)
.translateX(root.translationX)
}
@@ -336,7 +328,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
val projection = Projection.relativeToParent(
coordinateRoot,
binding.footerBackground,
binding.conversationItemFooterBackground,
shapeDelegate.corners
)
@@ -351,7 +343,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
val projection = Projection.relativeToParent(
coordinateRoot,
binding.bodyWrapper,
binding.conversationItemBodyWrapper,
shapeDelegate.corners
)
@@ -364,8 +356,8 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
}
private fun presentBody() {
binding.body.setTextColor(themeDelegate.getBodyTextColor(conversationMessage))
binding.body.setLinkTextColor(themeDelegate.getBodyTextColor(conversationMessage))
binding.conversationItemBody.setTextColor(themeDelegate.getBodyTextColor(conversationMessage))
binding.conversationItemBody.setLinkTextColor(themeDelegate.getBodyTextColor(conversationMessage))
val record = conversationMessage.messageRecord
var styledText: Spannable = conversationMessage.getDisplayBody(context)
@@ -375,21 +367,21 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
styledText = SearchUtil.getHighlightedSpan(Locale.getDefault(), STYLE_FACTORY, styledText, conversationContext.searchQuery, SearchUtil.STRICT)
if (record.hasExtraText()) {
binding.body.setOverflowText(getLongMessageSpan())
binding.conversationItemBody.setOverflowText(getLongMessageSpan())
} else {
binding.body.setOverflowText(null)
binding.conversationItemBody.setOverflowText(null)
}
if (isContentCondensed()) {
binding.body.maxLines = CONDENSED_MODE_MAX_LINES
binding.conversationItemBody.maxLines = CONDENSED_MODE_MAX_LINES
} else {
binding.body.maxLines = Integer.MAX_VALUE
binding.conversationItemBody.maxLines = Integer.MAX_VALUE
}
val bodyText = StringUtil.trim(styledText)
binding.body.visible = bodyText.isNotEmpty()
binding.body.text = bodyText
binding.conversationItemBody.visible = bodyText.isNotEmpty()
binding.conversationItemBody.text = bodyText
}
private fun linkifyMessageBody(messageBody: Spannable) {
@@ -456,17 +448,23 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
}
private fun isContentCondensed(): Boolean {
return conversationContext.displayMode is ConversationItemDisplayMode.Condensed && conversationContext.getPreviousMessage(bindingAdapterPosition) == null
return conversationContext.displayMode == ConversationItemDisplayMode.CONDENSED && conversationContext.getPreviousMessage(bindingAdapterPosition) == null
}
private fun presentFooterExpiry() {
val timer = binding.footerExpiry
if (shape == V2ConversationItemShape.MessageShape.MIDDLE || shape == V2ConversationItemShape.MessageShape.START) {
binding.conversationItemFooterExpiry.stopAnimation()
binding.conversationItemFooterExpiry.visible = false
return
}
binding.conversationItemFooterExpiry.setColorFilter(themeDelegate.getFooterIconColor(conversationMessage))
val timer = binding.conversationItemFooterExpiry
val record = conversationMessage.messageRecord
if (record.expiresIn > 0 && !record.isPending) {
timer.setColorFilter(themeDelegate.getFooterTextColor(conversationMessage), PorterDuff.Mode.SRC_IN)
timer.visible = true
timer.setPercentComplete(0f)
binding.conversationItemFooterExpiry.visible = true
binding.conversationItemFooterExpiry.setPercentComplete(0f)
if (record.expireStarted > 0) {
timer.setExpirationTime(record.expireStarted, record.expiresIn)
@@ -479,36 +477,10 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
conversationContext.onStartExpirationTimeout(record)
}
} else {
timer.stopAnimation()
timer.visible = false
}
}
private fun presentFooterEndPadding() {
binding.footerSpace?.visibility = if (isForcedFooter() || shape.isEndingShape) {
View.INVISIBLE
} else {
View.GONE
}
}
private fun presentSenderNameBackground() {
if (binding.senderName == null || !shape.isStartingShape || !conversationMessage.threadRecipient.isGroup || !conversationMessage.messageRecord.hasNoBubble(context)) {
return
}
if (conversationContext.hasWallpaper()) {
senderDrawable.setChatColors(
ChatColors.forColor(ChatColors.Id.BuiltIn, themeDelegate.getFooterBubbleColor(conversationMessage)),
footerCorners
)
binding.senderName.background = senderDrawable
} else {
binding.senderName.background = null
}
}
private fun presentSender() {
if (binding.senderName == null || binding.senderPhoto == null || binding.senderBadge == null) {
return
@@ -553,14 +525,14 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
private fun presentAlert() {
val record = conversationMessage.messageRecord
binding.body.setCompoundDrawablesWithIntrinsicBounds(
binding.conversationItemBody.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
if (record.isKeyExchange) R.drawable.ic_menu_login else 0,
0
)
val alert = binding.alert ?: return
val alert = binding.conversationItemAlert ?: return
when {
record.isFailed -> alert.setFailed()
@@ -578,26 +550,25 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
private fun presentReactions() {
if (conversationMessage.messageRecord.reactions.isEmpty()) {
binding.reactions.clear()
binding.conversationItemReactions.clear()
binding.root.removeOnMeasureListener(reactionMeasureListener)
} else {
binding.reactions.setReactions(conversationMessage.messageRecord.reactions)
binding.conversationItemReactions.setReactions(conversationMessage.messageRecord.reactions)
binding.root.addOnMeasureListener(reactionMeasureListener)
}
}
private fun presentFooterBackground() {
if (!binding.body.isJumbomoji || !conversationContext.hasWallpaper()) {
binding.footerBackground.visible = false
if (!binding.conversationItemBody.isJumbomoji ||
!conversationContext.hasWallpaper() ||
shape == V2ConversationItemShape.MessageShape.MIDDLE ||
shape == V2ConversationItemShape.MessageShape.START
) {
binding.conversationItemFooterBackground.visible = false
return
}
if (!(isForcedFooter() || shape.isEndingShape)) {
binding.footerBackground.visible = false
return
}
binding.footerBackground.visible = true
binding.conversationItemFooterBackground.visible = true
footerDrawable.setChatColors(
if (binding.isIncoming) {
ChatColors.forColor(ChatColors.Id.NotSet, themeDelegate.getFooterBubbleColor(conversationMessage))
@@ -609,16 +580,13 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
}
private fun presentDate() {
if (!shape.isEndingShape && !isForcedFooter()) {
binding.footerDate.visible = false
if (shape == V2ConversationItemShape.MessageShape.MIDDLE || shape == V2ConversationItemShape.MessageShape.START) {
binding.conversationItemFooterDate.visible = false
return
}
dateString = conversationMessage.computedProperties.formattedDate.value
binding.footerDate.setOnClickListener(null)
binding.footerDate.visible = true
binding.footerDate.setTextColor(themeDelegate.getFooterTextColor(conversationMessage))
binding.conversationItemFooterDate.visible = true
binding.conversationItemFooterDate.setTextColor(themeDelegate.getFooterTextColor(conversationMessage))
val record = conversationMessage.messageRecord
if (record.isFailed) {
@@ -628,31 +596,27 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
else -> R.string.ConversationItem_error_not_sent_tap_for_details
}
binding.footerDate.setText(errorMessage)
binding.conversationItemFooterDate.setText(errorMessage)
} else if (record.isPendingInsecureSmsFallback) {
binding.footerDate.setText(R.string.ConversationItem_click_to_approve_unencrypted)
binding.conversationItemFooterDate.setText(R.string.ConversationItem_click_to_approve_unencrypted)
} else if (record.isRateLimited) {
binding.footerDate.setText(R.string.ConversationItem_send_paused)
binding.conversationItemFooterDate.setText(R.string.ConversationItem_send_paused)
} else if (record.isScheduled()) {
binding.footerDate.text = conversationMessage.computedProperties.formattedDate.value
binding.conversationItemFooterDate.text = conversationMessage.formattedDate
} else {
var date = dateString
if (conversationContext.displayMode != ConversationItemDisplayMode.Detailed && record is MediaMmsMessageRecord && record.isEditMessage()) {
var date = conversationMessage.formattedDate
if (conversationContext.displayMode != ConversationItemDisplayMode.DETAILED && record is MediaMmsMessageRecord && record.isEditMessage()) {
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date)
binding.footerDate.setOnClickListener {
conversationContext.clickListener.onEditedIndicatorClicked(record)
}
}
binding.footerDate.text = date
binding.conversationItemFooterDate.text = date
}
}
private fun presentDeliveryStatus() {
val deliveryStatus = binding.deliveryStatus ?: return
val deliveryStatus = binding.conversationItemDeliveryStatus ?: return
if (!shape.isEndingShape && !isForcedFooter()) {
if (shape == V2ConversationItemShape.MessageShape.MIDDLE || shape == V2ConversationItemShape.MessageShape.START) {
deliveryStatus.setNone()
return
}
@@ -703,15 +667,11 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
return false
}
private fun isForcedFooter(): Boolean {
return conversationMessage.messageRecord.isEditMessage || conversationMessage.messageRecord.expiresIn > 0L
}
private inner class ReactionMeasureListener : V2ConversationItemLayout.OnMeasureListener {
override fun onPreMeasure() = Unit
override fun onPostMeasure(): Boolean {
return binding.reactions.setBubbleWidth(binding.bodyWrapper.measuredWidth)
return binding.conversationItemReactions.setBubbleWidth(binding.conversationItemBodyWrapper.measuredWidth)
}
}
@@ -721,7 +681,7 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
}
override fun onLongClick(v: View?): Boolean {
if (binding.body.hasSelection()) {
if (binding.conversationItemBody.hasSelection()) {
return false
}

Some files were not shown because too many files have changed in this diff Show More