mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-14 22:13:19 +01:00
Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ccfab4087 | ||
|
|
a45ce55808 | ||
|
|
c7dabe1b6f | ||
|
|
ec51268439 | ||
|
|
7543b9fa37 | ||
|
|
ca3187d0b8 | ||
|
|
327cd93e3c | ||
|
|
13853c708e | ||
|
|
ee1291c816 | ||
|
|
6d2d3ae528 | ||
|
|
784f94ecdb | ||
|
|
93bf853b5e | ||
|
|
bb83ddfe28 | ||
|
|
b51ec53e33 | ||
|
|
ca210f2b6d | ||
|
|
38bddec4ba | ||
|
|
b866d57814 | ||
|
|
3c9004d87d | ||
|
|
c479dd404c | ||
|
|
748667a0b4 | ||
|
|
6898595f8a | ||
|
|
30d0b6fd0e | ||
|
|
7c209db146 | ||
|
|
49c8c88a22 | ||
|
|
88e530c96c | ||
|
|
14f3fb5a94 | ||
|
|
7ac479b78a | ||
|
|
10b356e642 | ||
|
|
7f92482d7a | ||
|
|
b79a7309aa | ||
|
|
b54781ff56 | ||
|
|
6a87495a6d | ||
|
|
c5d9346370 | ||
|
|
d247e2c111 | ||
|
|
b30f47bac4 | ||
|
|
2f9498e137 | ||
|
|
067b3513b7 | ||
|
|
e50ed22c85 | ||
|
|
7cf17f3cc4 | ||
|
|
9f52ecab5c | ||
|
|
c8a56d4f78 | ||
|
|
71d482ab29 | ||
|
|
1cc7b46555 | ||
|
|
f56a65d30d | ||
|
|
aff813b284 | ||
|
|
181c0e8a60 | ||
|
|
cf7f614296 | ||
|
|
351e37bcee | ||
|
|
cc1f27f588 | ||
|
|
859905c3e4 | ||
|
|
8af91bffb5 | ||
|
|
06dc8ccbdd | ||
|
|
c501a417bb | ||
|
|
0021e229d8 | ||
|
|
b4ef95a9b4 | ||
|
|
f25a716d62 | ||
|
|
a9739ed500 | ||
|
|
a131eeaa4a | ||
|
|
9382bbd8fd | ||
|
|
adb1e292bf | ||
|
|
ca79929141 | ||
|
|
ae2998bcf2 | ||
|
|
1e8e09d5c4 | ||
|
|
a5e30bc818 | ||
|
|
72edf5c08b | ||
|
|
192154a11c | ||
|
|
c3700cf6d9 | ||
|
|
78bdee61ef | ||
|
|
b84eea9620 | ||
|
|
5f289fa400 | ||
|
|
67b8f468e4 | ||
|
|
9c49c84306 | ||
|
|
b31ee802fc | ||
|
|
b893a0eb76 | ||
|
|
6f1a04abce | ||
|
|
041ba27efe | ||
|
|
e239036d8b | ||
|
|
d3f073e573 | ||
|
|
0b7490dc06 | ||
|
|
a0e514dac9 | ||
|
|
2f1eaf7d6b | ||
|
|
6cb8b1c439 | ||
|
|
5363208e4e | ||
|
|
510ff51198 | ||
|
|
6dde3d55ef | ||
|
|
e3ec53c2d0 | ||
|
|
e7972d4903 | ||
|
|
a2c3b5d64e | ||
|
|
b11d653fc0 | ||
|
|
66792f2d56 | ||
|
|
c012ead143 | ||
|
|
82906aee58 | ||
|
|
7ff4a82755 | ||
|
|
8ca49c1e18 | ||
|
|
7d68a57f53 | ||
|
|
c68487c0c7 | ||
|
|
4adc660705 | ||
|
|
d78e73bd6f | ||
|
|
9d33690f34 | ||
|
|
4c428e5b5b | ||
|
|
4c3882689f | ||
|
|
c82ed473fc | ||
|
|
06664b4c58 | ||
|
|
5e68388b01 | ||
|
|
e14fcf8577 | ||
|
|
0219c5253b | ||
|
|
adf3d74d91 | ||
|
|
3acd68e0b3 | ||
|
|
e5eccd732d | ||
|
|
eb0df5791a | ||
|
|
f5371123da | ||
|
|
6aa723bc22 | ||
|
|
9ba34df4ae | ||
|
|
6194515f8e | ||
|
|
39289715bc | ||
|
|
5fdd2430ca | ||
|
|
a7f3f485ad | ||
|
|
69a76fa1b7 | ||
|
|
2e5e64b05d | ||
|
|
933e3233a7 | ||
|
|
a54df29542 | ||
|
|
cdce802b32 | ||
|
|
2abf30e94b | ||
|
|
148cff1b48 | ||
|
|
ce2a21c438 | ||
|
|
d77c0198d1 | ||
|
|
f58a1acff5 | ||
|
|
aadbddd7e9 | ||
|
|
689fe3947b | ||
|
|
fff0b8b187 | ||
|
|
74562432cf | ||
|
|
8e3027642b | ||
|
|
39f96bb12c | ||
|
|
938309d125 | ||
|
|
f740b69ffe | ||
|
|
0a4147aa0e | ||
|
|
dcffc13843 | ||
|
|
1e9a0cdc16 | ||
|
|
82e7050864 | ||
|
|
72d1e55373 | ||
|
|
fe5d5df2d7 | ||
|
|
9bc337373e | ||
|
|
5aad879a95 | ||
|
|
a3798dba68 | ||
|
|
b9f7ef5cbd | ||
|
|
0c3b541031 | ||
|
|
a09bc53b99 | ||
|
|
3731723472 | ||
|
|
ded29619cd | ||
|
|
a5b39a8f17 | ||
|
|
26866a7b2c | ||
|
|
7837f3999f | ||
|
|
b25f658647 | ||
|
|
49625619fe | ||
|
|
912299bcfd | ||
|
|
5648fd2e91 | ||
|
|
557ef5820e | ||
|
|
4ce512d259 | ||
|
|
a68319dae4 | ||
|
|
080ecf51d3 | ||
|
|
657109dae1 | ||
|
|
019ef02be8 | ||
|
|
d4774c963d | ||
|
|
34cb4c579c | ||
|
|
74c261f913 | ||
|
|
7420123519 | ||
|
|
374910736e | ||
|
|
3a71696a49 | ||
|
|
73792905a2 | ||
|
|
05fc30e6e8 | ||
|
|
9c308588b5 | ||
|
|
2f53200096 | ||
|
|
8c1f2c6064 | ||
|
|
f5fc2acf50 | ||
|
|
306fa24d6b | ||
|
|
f5ee9d4a3b | ||
|
|
e7a5f64fe5 | ||
|
|
6191e003fc | ||
|
|
fad401941e | ||
|
|
1e0733bd46 | ||
|
|
d0a44c3f14 | ||
|
|
3cee0c1bd5 | ||
|
|
f5d403e97d | ||
|
|
4f1d021aa8 | ||
|
|
dca8c042ab | ||
|
|
ae1ccadcc8 | ||
|
|
9ac12c2532 | ||
|
|
18337c97e2 | ||
|
|
1e652d497e | ||
|
|
b53cad2808 | ||
|
|
4520ff78ff | ||
|
|
b887129cd7 | ||
|
|
ec25831a37 | ||
|
|
744f74b498 | ||
|
|
52aaf93f37 | ||
|
|
2d92d4ad87 | ||
|
|
7617cc0a80 | ||
|
|
dc69bcf6f2 | ||
|
|
0775fc7ead | ||
|
|
034aef483b | ||
|
|
ee5b99fed4 | ||
|
|
e031da1337 | ||
|
|
8f1514642c | ||
|
|
5aa304ea9a | ||
|
|
c5a27b2cc7 | ||
|
|
0fde404da8 | ||
|
|
5242b9af39 | ||
|
|
5e2d6fc05f | ||
|
|
7e08a1f321 | ||
|
|
076295eae8 | ||
|
|
c13339ca52 | ||
|
|
627657e1de | ||
|
|
a8349671d0 | ||
|
|
461875b0e4 | ||
|
|
00bbb6bc6e | ||
|
|
e1f1181a07 | ||
|
|
0e1de39192 | ||
|
|
05bbeb10da | ||
|
|
7375a9e06b | ||
|
|
4910050891 | ||
|
|
67d4f666ce | ||
|
|
e6c9449e3c | ||
|
|
b8effba497 | ||
|
|
8fcdd7cb8a | ||
|
|
f3fb5ccc3b | ||
|
|
b8f55f982f | ||
|
|
6db59cb896 | ||
|
|
3db83c1602 | ||
|
|
6be9225fbd | ||
|
|
653eff403c | ||
|
|
ab410ec0cf | ||
|
|
7b75a32394 | ||
|
|
daf077b3c9 | ||
|
|
f6bbb59400 | ||
|
|
09813d5dbd | ||
|
|
fe509838f4 | ||
|
|
6a443d0074 | ||
|
|
8fc1065dd6 | ||
|
|
1af50ba0f5 | ||
|
|
82b3036b77 | ||
|
|
676412019c | ||
|
|
980f4e00e2 | ||
|
|
5731bf023a | ||
|
|
2511ca17aa | ||
|
|
fae653540b | ||
|
|
b0ca66cc1a | ||
|
|
a65e9c76bc | ||
|
|
adbac4c557 | ||
|
|
fc7b024e96 | ||
|
|
acee65ba25 | ||
|
|
244902ecfc | ||
|
|
4b1a678af2 | ||
|
|
59dd72b5c0 | ||
|
|
44c393f11a | ||
|
|
fddfbd8d2d | ||
|
|
e7e00bd428 | ||
|
|
07702e69ad | ||
|
|
e5c3757629 | ||
|
|
7031bbae43 | ||
|
|
4a6dfed676 | ||
|
|
bb0b414d71 | ||
|
|
95e25652c1 | ||
|
|
58155b0859 | ||
|
|
f579b79d2e |
@@ -5,4 +5,5 @@ indent_size = 2
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = false
|
||||
ij_kotlin_allow_trailing_comma = false
|
||||
ktlint_code_style = intellij_idea
|
||||
twitter_compose_allowed_composition_locals=LocalExtendedColors
|
||||
twitter_compose_allowed_composition_locals=LocalExtendedColors
|
||||
ktlint_standard_class-naming = disabled
|
||||
@@ -45,8 +45,8 @@ ktlint {
|
||||
version = "0.49.1"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1290
|
||||
def canonicalVersionName = "6.26.0"
|
||||
def canonicalVersionCode = 1311
|
||||
def canonicalVersionName = "6.29.0"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -382,7 +382,7 @@ android {
|
||||
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\""
|
||||
buildConfigField "String", "SVR2_MRENCLAVE", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
|
||||
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
|
||||
"\"ee1d0d972b7ea903615670de43ab1b6e7a825e811c70a29bb5fe0f819e0975fa\", " +
|
||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
|
||||
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.testing.success
|
||||
import org.thoughtcrime.securesms.testing.timeout
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.internal.push.MismatchedDevices
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyState
|
||||
import java.util.UUID
|
||||
@@ -73,7 +74,7 @@ class ChangeNumberViewModelTest {
|
||||
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
@@ -180,7 +181,7 @@ class ChangeNumberViewModelTest {
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val oldPni = Recipient.self().requirePni()
|
||||
val oldE164 = Recipient.self().requireE164()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
@@ -225,7 +226,7 @@ class ChangeNumberViewModelTest {
|
||||
fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
@@ -269,7 +270,7 @@ class ChangeNumberViewModelTest {
|
||||
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
@@ -313,7 +314,7 @@ class ChangeNumberViewModelTest {
|
||||
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.from(UUID.randomUUID())
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
|
||||
@@ -9,10 +9,13 @@ import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.mms.MediaStream
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.util.Optional
|
||||
|
||||
@@ -92,6 +95,45 @@ class AttachmentTableTest {
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given: A previous attachment and two pre-upload attachments with the same data but different transform properties (standard and high).
|
||||
*
|
||||
* When changing content of standard pre-upload attachment to match pre-existing attachment
|
||||
*
|
||||
* Then update standard pre-upload attachment to match previous attachment, do not update high pre-upload attachment, and do
|
||||
* not delete shared pre-upload uri from disk as it is still being used by the high pre-upload attachment.
|
||||
*/
|
||||
@Test
|
||||
fun doNotDeleteDedupedFileIfUsedByAnotherAttachmentWithADifferentTransformProperties() {
|
||||
// GIVEN
|
||||
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
||||
val compressedData = byteArrayOf(1, 2, 3)
|
||||
|
||||
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
|
||||
|
||||
val previousAttachment = createAttachment(1, BlobProvider.getInstance().forData(compressedData).createForSingleSessionInMemory(), AttachmentTable.TransformProperties.empty())
|
||||
val previousDatabaseAttachmentId: AttachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(1, listOf(previousAttachment), emptyList()).values.first()
|
||||
|
||||
val standardQualityPreUpload = createAttachment(1, blobUncompressed, AttachmentTable.TransformProperties.empty())
|
||||
val standardDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(standardQualityPreUpload)
|
||||
|
||||
val highQualityPreUpload = createAttachment(1, blobUncompressed, AttachmentTable.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH))
|
||||
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData), false)
|
||||
|
||||
// THEN
|
||||
val previousInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(previousDatabaseAttachmentId, AttachmentTable.DATA)!!
|
||||
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
||||
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
||||
|
||||
assertNotEquals(standardInfo, highInfo)
|
||||
standardInfo.file assertIs previousInfo.file
|
||||
highInfo.file assertIsNot standardInfo.file
|
||||
highInfo.file.exists() assertIs true
|
||||
}
|
||||
|
||||
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties): UriAttachment {
|
||||
return UriAttachmentBuilder.build(
|
||||
id,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CallLinkTableTest {
|
||||
|
||||
companion object {
|
||||
private val ROOM_ID_A = byteArrayOf(1, 2, 3, 4)
|
||||
private val ROOM_ID_B = byteArrayOf(2, 2, 3, 4)
|
||||
private const val TIMESTAMP_A = 1000L
|
||||
private const val TIMESTAMP_B = 2000L
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteBeforeFirst_thenIExpectNeitherDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_A - 500)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(2, callEvents.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteOnFirst_thenIExpectFirstDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_A)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(1, callEvents.size)
|
||||
assertEquals(TIMESTAMP_B, callEvents.first().record.timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteAfterFirstAndBeforeSecond_thenIExpectFirstDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_B - 500)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(1, callEvents.size)
|
||||
assertEquals(TIMESTAMP_B, callEvents.first().record.timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteOnSecond_thenIExpectBothDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_B)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(0, callEvents.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteAfterSecond_thenIExpectBothDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_B + 500)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(0, callEvents.size)
|
||||
}
|
||||
|
||||
private fun insertTwoNonAdminCallLinksWithEvents() {
|
||||
insertCallLinkWithEvent(ROOM_ID_A, 1000)
|
||||
insertCallLinkWithEvent(ROOM_ID_B, 2000)
|
||||
}
|
||||
|
||||
private fun insertCallLinkWithEvent(roomId: ByteArray, timestamp: Long) {
|
||||
SignalDatabase.callLinks.insertCallLink(
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = CallLinkRoomId.fromBytes(roomId),
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = roomId,
|
||||
adminPassBytes = null
|
||||
),
|
||||
state = SignalCallLinkState()
|
||||
)
|
||||
)
|
||||
|
||||
val callLinkRecipient = SignalDatabase.recipients.getByCallLinkRoomId(CallLinkRoomId.fromBytes(roomId)).get()
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
1,
|
||||
callLinkRecipient,
|
||||
CallTable.Direction.INCOMING,
|
||||
timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.ringrtc.CallId
|
||||
import org.signal.ringrtc.CallManager
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@@ -751,4 +752,74 @@ class CallTableTest {
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteBeforeCallB_thenOnlyDeleteCallA() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(1500)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(1, allCallEvents.size)
|
||||
assertEquals(2, allCallEvents.first().record.callId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteBeforeCallA_thenIDoNotDeleteAnyCalls() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(500)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(2, allCallEvents.size)
|
||||
assertEquals(2, allCallEvents[0].record.callId)
|
||||
assertEquals(1, allCallEvents[1].record.callId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteOnCallA_thenIOnlyDeleteCallA() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(1000)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(1, allCallEvents.size)
|
||||
assertEquals(2, allCallEvents.first().record.callId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteOnCallB_thenIDeleteBothCalls() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(2000)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(0, allCallEvents.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteAfterCallB_thenIDeleteBothCalls() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(2500)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(0, allCallEvents.size)
|
||||
}
|
||||
|
||||
private fun insertTwoCallEvents() {
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
1,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
1000
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
2,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.OUTGOING,
|
||||
2000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
class DistributionListTablesTest {
|
||||
|
||||
@@ -294,12 +294,12 @@ class GroupTableTest {
|
||||
private fun insertPushGroup(
|
||||
members: List<DecryptedMember> = listOf(
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(harness.self.requireServiceId().toByteString())
|
||||
.setAciBytes(harness.self.requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build(),
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(Recipient.resolved(harness.others[0]).requireServiceId().toByteString())
|
||||
.setAciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
@@ -318,14 +318,14 @@ class GroupTableTest {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
|
||||
val selfMember: DecryptedMember = DecryptedMember.newBuilder()
|
||||
.setUuid(harness.self.requireServiceId().toByteString())
|
||||
.setAciBytes(harness.self.requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
|
||||
val otherMembers: List<DecryptedMember> = others.map { id ->
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(Recipient.resolved(id).requireServiceId().toByteString())
|
||||
.setAciBytes(Recipient.resolved(id).requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
|
||||
@@ -10,9 +10,8 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@@ -34,7 +33,7 @@ class MessageTableTest_gifts {
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -17,7 +17,6 @@ object MmsHelper {
|
||||
recipient: Recipient = Recipient.UNKNOWN,
|
||||
body: String = "body",
|
||||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
subscriptionId: Int = -1,
|
||||
expiresIn: Long = 0,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
|
||||
@@ -32,7 +31,6 @@ object MmsHelper {
|
||||
recipient = recipient,
|
||||
body = body,
|
||||
timestamp = sentTimeMillis,
|
||||
subscriptionId = subscriptionId,
|
||||
expiresIn = expiresIn,
|
||||
viewOnce = viewOnce,
|
||||
distributionType = distributionType,
|
||||
|
||||
@@ -16,9 +16,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -45,7 +44,7 @@ class MmsTableTest_stories {
|
||||
SignalStore.account().setPni(localPni)
|
||||
|
||||
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
|
||||
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
|
||||
|
||||
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelRecipient.id)
|
||||
|
||||
@@ -13,8 +13,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -24,14 +24,14 @@ class RecipientTableTest {
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun givenAHiddenRecipient_whenIQueryAllContacts_thenIDoNotExpectHiddenToBeReturned() {
|
||||
fun givenAHiddenRecipient_whenIQueryAllContacts_thenIExpectHiddenToBeReturned() {
|
||||
val hiddenRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||
|
||||
val results = SignalDatabase.recipients.queryAllContacts("Hidden")!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
assertEquals(1, results.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -173,10 +173,10 @@ class RecipientTableTest {
|
||||
|
||||
SignalDatabase.recipients.markUnregistered(mainId)
|
||||
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
assertEquals(mainId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
@@ -192,10 +192,10 @@ class RecipientTableTest {
|
||||
|
||||
SignalDatabase.recipients.splitForStorageSync(mainRecord.storageId!!)
|
||||
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
assertEquals(mainId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.hamcrest.MatcherAssert
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
@@ -14,6 +15,7 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.exists
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.select
|
||||
@@ -36,14 +38,13 @@ import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@@ -59,6 +60,21 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun single() {
|
||||
test("merge, e164 + pni reassigned, aci abandoned") {
|
||||
given(E164_A, PNI_A, ACI_A)
|
||||
given(E164_B, PNI_B, ACI_B)
|
||||
|
||||
process(E164_A, PNI_A, ACI_B)
|
||||
|
||||
expect(null, null, ACI_A)
|
||||
expect(E164_A, PNI_A, ACI_B)
|
||||
|
||||
expectChangeNumberEvent()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allNonMergeTests() {
|
||||
test("e164-only insert") {
|
||||
@@ -69,7 +85,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
assertEquals(RecipientTable.RegisteredState.UNKNOWN, record.registered)
|
||||
}
|
||||
|
||||
test("pni-only insert") {
|
||||
test("pni-only insert", exception = IllegalArgumentException::class.java) {
|
||||
val id = process(null, PNI_A, null)
|
||||
expect(null, PNI_A, null)
|
||||
|
||||
@@ -84,6 +100,21 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
val record = SignalDatabase.recipients.getRecord(id)
|
||||
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
|
||||
}
|
||||
|
||||
test("e164+pni insert") {
|
||||
process(E164_A, PNI_A, null)
|
||||
expect(E164_A, PNI_A, null)
|
||||
}
|
||||
|
||||
test("e164+aci insert") {
|
||||
process(E164_A, null, ACI_A)
|
||||
expect(E164_A, null, ACI_A)
|
||||
}
|
||||
|
||||
test("e164+pni+aci insert") {
|
||||
process(E164_A, PNI_A, ACI_A)
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -167,6 +198,12 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectSessionSwitchoverEvent(E164_A)
|
||||
}
|
||||
|
||||
test("e164 and pni matches, all provided, new aci, existing pni session, pni-verified") {
|
||||
given(E164_A, PNI_A, null, pniSession = true)
|
||||
process(E164_A, PNI_A, ACI_A, pniVerified = true)
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
}
|
||||
|
||||
test("e164 and aci matches, all provided, new pni") {
|
||||
given(E164_A, null, ACI_A)
|
||||
process(E164_A, PNI_A, ACI_A)
|
||||
@@ -309,6 +346,26 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectChangeNumberEvent()
|
||||
}
|
||||
|
||||
test("steal, pni is changed") {
|
||||
given(E164_A, PNI_B, ACI_A)
|
||||
given(E164_B, PNI_A, null)
|
||||
|
||||
process(E164_A, PNI_A, null)
|
||||
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
expect(E164_B, null, null)
|
||||
}
|
||||
|
||||
test("steal, pni is changed, aci left behind") {
|
||||
given(E164_B, PNI_A, ACI_A)
|
||||
given(E164_A, PNI_B, null)
|
||||
|
||||
process(E164_A, PNI_A, null)
|
||||
|
||||
expect(E164_B, null, ACI_A)
|
||||
expect(E164_A, PNI_A, null)
|
||||
}
|
||||
|
||||
test("steal, e164+pni & e164+pni, no aci provided, no pni session") {
|
||||
given(E164_A, PNI_B, null)
|
||||
given(E164_B, PNI_A, null)
|
||||
@@ -502,7 +559,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectThreadMergeEvent(E164_A)
|
||||
}
|
||||
|
||||
test("merge, e164+pni & aci, pni session, thread merge shadows") {
|
||||
test("merge, e164+pni & aci, pni session, thread merge shadows SSE") {
|
||||
given(E164_A, PNI_A, null, pniSession = true)
|
||||
given(null, null, ACI_A)
|
||||
|
||||
@@ -600,6 +657,18 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectThreadMergeEvent(E164_A)
|
||||
}
|
||||
|
||||
test("merge, e164 + pni reassigned, aci abandoned") {
|
||||
given(E164_A, PNI_A, ACI_A)
|
||||
given(E164_B, PNI_B, ACI_B)
|
||||
|
||||
process(E164_A, PNI_A, ACI_B)
|
||||
|
||||
expect(null, null, ACI_A)
|
||||
expect(E164_A, PNI_A, ACI_B)
|
||||
|
||||
expectChangeNumberEvent()
|
||||
}
|
||||
|
||||
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
|
||||
given(E164_SELF, null, ACI_SELF)
|
||||
given(null, null, ACI_A)
|
||||
@@ -768,9 +837,15 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
}
|
||||
|
||||
private fun identityKey(value: Byte): IdentityKey {
|
||||
val byteArray = ByteArray(32)
|
||||
byteArray[0] = value
|
||||
return identityKey(byteArray)
|
||||
}
|
||||
|
||||
private fun identityKey(value: ByteArray): IdentityKey {
|
||||
val bytes = ByteArray(33)
|
||||
bytes[0] = 0x05
|
||||
bytes[1] = value
|
||||
value.copyInto(bytes, 1)
|
||||
return IdentityKey(bytes)
|
||||
}
|
||||
|
||||
@@ -873,8 +948,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
generatedIds += id
|
||||
if (createThread) {
|
||||
// Create a thread and throw a dummy message in it so it doesn't get automatically deleted
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(id))
|
||||
SignalDatabase.messages.insertMessageInbox(IncomingEncryptedMessage(IncomingTextMessage(id, 1, (Math.random() * Long.MAX_VALUE).toLong(), 0, 0, "", Optional.empty(), 0, false, ""), ""))
|
||||
val result = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = id, time = (Math.random() * 10000000).toLong(), body = "1"))
|
||||
SignalDatabase.threads.markAsActiveEarly(result.get().threadId)
|
||||
}
|
||||
|
||||
if (pniSession) {
|
||||
@@ -885,11 +960,34 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
SignalDatabase.sessions.store(pni, SignalProtocolAddress(pni.toString(), 1), SessionRecord())
|
||||
}
|
||||
|
||||
if (aci != null) {
|
||||
SignalDatabase.identities.saveIdentity(
|
||||
addressName = aci.toString(),
|
||||
recipientId = id,
|
||||
identityKey = identityKey(Util.getSecretBytes(32)),
|
||||
verifiedStatus = IdentityTable.VerifiedStatus.DEFAULT,
|
||||
firstUse = true,
|
||||
timestamp = 0,
|
||||
nonBlockingApproval = false
|
||||
)
|
||||
}
|
||||
if (pni != null) {
|
||||
SignalDatabase.identities.saveIdentity(
|
||||
addressName = pni.toString(),
|
||||
recipientId = id,
|
||||
identityKey = identityKey(Util.getSecretBytes(32)),
|
||||
verifiedStatus = IdentityTable.VerifiedStatus.DEFAULT,
|
||||
firstUse = true,
|
||||
timestamp = 0,
|
||||
nonBlockingApproval = false
|
||||
)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
fun process(e164: String?, pni: PNI?, aci: ACI?, changeSelf: Boolean = false, pniVerified: Boolean = false): RecipientId {
|
||||
outputRecipientId = SignalDatabase.recipients.getAndPossiblyMerge(serviceId = aci ?: pni, pni = pni, e164 = e164, pniVerified = pniVerified, changeSelf = changeSelf)
|
||||
outputRecipientId = SignalDatabase.recipients.getAndPossiblyMerge(aci = aci, pni = pni, e164 = e164, pniVerified = pniVerified, changeSelf = changeSelf)
|
||||
generatedIds += outputRecipientId
|
||||
return outputRecipientId
|
||||
}
|
||||
@@ -903,15 +1001,15 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
val expected = RecipientTuple(
|
||||
e164 = e164,
|
||||
pni = pni,
|
||||
serviceId = aci ?: pni
|
||||
aci = aci
|
||||
)
|
||||
val actual = RecipientTuple(
|
||||
e164 = recipient.e164.orElse(null),
|
||||
pni = recipient.pni.orElse(null),
|
||||
serviceId = recipient.serviceId.orElse(null)
|
||||
aci = recipient.aci.orElse(null)
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
assertEquals("Recipient $id did not match expected result!", expected, actual)
|
||||
}
|
||||
|
||||
fun expectDeleted() {
|
||||
@@ -919,21 +1017,21 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
}
|
||||
|
||||
fun expectDeleted(id: RecipientId) {
|
||||
SignalDatabase.rawDatabase
|
||||
.select("1")
|
||||
.from(RecipientTable.TABLE_NAME)
|
||||
val found = SignalDatabase.rawDatabase
|
||||
.exists(RecipientTable.TABLE_NAME)
|
||||
.where("${RecipientTable.ID} = ?", id)
|
||||
.run()
|
||||
.use { !it.moveToFirst() }
|
||||
|
||||
assertFalse("Expected $id to be deleted, but it's still present!", found)
|
||||
}
|
||||
|
||||
fun expectChangeNumberEvent() {
|
||||
assertEquals(1, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
|
||||
assertEquals("Missing change number event!", 1, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
|
||||
changeNumberExpected = true
|
||||
}
|
||||
|
||||
fun expectNoChangeNumberEvent() {
|
||||
assertEquals(0, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
|
||||
assertEquals("Unexpected change number event!", 0, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
|
||||
changeNumberExpected = false
|
||||
}
|
||||
|
||||
@@ -943,42 +1041,39 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
fun expectSessionSwitchoverEvent(recipientId: RecipientId, e164: String) {
|
||||
val event: SessionSwitchoverEvent? = getLatestSessionSwitchoverEvent(recipientId)
|
||||
assertNotNull(event)
|
||||
assertNotNull("Missing session switchover event! Expected one with e164 = $e164", event)
|
||||
assertEquals(e164, event!!.e164)
|
||||
sessionSwitchoverExpected = true
|
||||
}
|
||||
|
||||
fun expectNoSessionSwitchoverEvent() {
|
||||
assertNull(getLatestSessionSwitchoverEvent(outputRecipientId))
|
||||
assertNull("Unexpected session switchover event!", getLatestSessionSwitchoverEvent(outputRecipientId))
|
||||
}
|
||||
|
||||
fun expectThreadMergeEvent(previousE164: String) {
|
||||
val event: ThreadMergeEvent? = getLatestThreadMergeEvent(outputRecipientId)
|
||||
assertNotNull(event)
|
||||
assertEquals(previousE164, event!!.previousE164)
|
||||
assertNotNull("Missing thread merge event! Expected one with e164 = $previousE164", event)
|
||||
assertEquals("E164 on thread merge event doesn't match!", previousE164, event!!.previousE164)
|
||||
threadMergeExpected = true
|
||||
}
|
||||
|
||||
fun expectNoThreadMergeEvent() {
|
||||
assertNull(getLatestThreadMergeEvent(outputRecipientId))
|
||||
assertNull("Unexpected thread merge event!", getLatestThreadMergeEvent(outputRecipientId))
|
||||
}
|
||||
|
||||
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
|
||||
val serviceIdString: String? = (aci ?: pni)?.toString()
|
||||
val pniString: String? = pni?.toString()
|
||||
|
||||
val id: Long = SignalDatabase.rawDatabase.insert(
|
||||
RecipientTable.TABLE_NAME,
|
||||
null,
|
||||
contentValuesOf(
|
||||
RecipientTable.PHONE to e164,
|
||||
RecipientTable.SERVICE_ID to serviceIdString,
|
||||
RecipientTable.PNI_COLUMN to pniString,
|
||||
RecipientTable.E164 to e164,
|
||||
RecipientTable.ACI_COLUMN to aci?.toString(),
|
||||
RecipientTable.PNI_COLUMN to pni?.toString(),
|
||||
RecipientTable.REGISTERED to RecipientTable.RegisteredState.REGISTERED.id
|
||||
)
|
||||
)
|
||||
|
||||
assertTrue("Failed to insert! E164: $e164, ServiceId: $serviceIdString, PNI: $pniString", id > 0)
|
||||
assertTrue("Failed to insert! E164: $e164, ACI: $aci, PNI: $pni", id > 0)
|
||||
|
||||
return RecipientId.from(id)
|
||||
}
|
||||
@@ -987,14 +1082,14 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
data class RecipientTuple(
|
||||
val e164: String?,
|
||||
val pni: PNI?,
|
||||
val serviceId: ServiceId?
|
||||
val aci: ACI?
|
||||
) {
|
||||
|
||||
/**
|
||||
* The intent here is to give nice diffs with the name of the constants rather than the values.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return "(${e164.e164String()}, ${pni.pniString()}, ${serviceId.serviceIdString()})"
|
||||
return "(${e164.e164String()}, ${pni.pniString()}, ${aci.aciString()})"
|
||||
}
|
||||
|
||||
private fun String?.e164String(): String {
|
||||
@@ -1018,12 +1113,9 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
} ?: "null"
|
||||
}
|
||||
|
||||
private fun ServiceId?.serviceIdString(): String {
|
||||
private fun ACI?.aciString(): String {
|
||||
return this?.let {
|
||||
when (it) {
|
||||
PNI_A -> "PNI_A"
|
||||
PNI_B -> "PNI_B"
|
||||
PNI_SELF -> "PNI_SELF"
|
||||
ACI_A -> "ACI_A"
|
||||
ACI_B -> "ACI_B"
|
||||
ACI_SELF -> "ACI_SELF"
|
||||
|
||||
@@ -21,9 +21,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@@ -283,8 +282,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val aliceServiceId: ServiceId = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
private val bobServiceId: ServiceId = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
private val aliceServiceId: ACI = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
private val bobServiceId: ACI = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
private val masterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
private val groupId = GroupId.v2(masterKey)
|
||||
|
||||
@@ -21,7 +21,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -465,7 +465,7 @@ class StorySendTableTest {
|
||||
|
||||
private fun makeRecipients(count: Int): List<RecipientId> {
|
||||
return (1..count).map {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
class ThreadTableTest_active {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule()
|
||||
|
||||
private lateinit var recipient: Recipient
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
|
||||
SignalDatabase.threads.getUnarchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
false,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(1, threads.count)
|
||||
|
||||
val record = ThreadTable.StaticReader(threads, InstrumentationRegistry.getInstrumentation().context).getNext()
|
||||
|
||||
assertNotNull(record)
|
||||
assertEquals(record!!.recipient.id, recipient.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenInactiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
|
||||
SignalDatabase.threads.getUnarchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
false,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(0, threads.count)
|
||||
}
|
||||
|
||||
val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
assertEquals(threadId2, threadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveArchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), true)
|
||||
|
||||
SignalDatabase.threads.getUnarchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
false,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(0, threads.count)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveArchivedThread_whenIGetArchivedConversationList_thenIExpectThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), true)
|
||||
|
||||
SignalDatabase.threads.getArchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(1, threads.count)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenInactiveArchivedThread_whenIGetArchivedConversationList_thenIExpectNoThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), true)
|
||||
|
||||
SignalDatabase.threads.getArchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(0, threads.count)
|
||||
}
|
||||
|
||||
val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
assertEquals(threadId2, threadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveArchivedThread_whenIDeactivateThread_thenIExpectNoMessages() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
|
||||
SignalDatabase.messages.getConversation(threadId).use {
|
||||
assertEquals(1, it.count)
|
||||
}
|
||||
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
|
||||
SignalDatabase.messages.getConversation(threadId).use {
|
||||
assertEquals(0, it.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import org.signal.core.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@@ -23,7 +23,7 @@ class ThreadTableTest_pinned {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.signal.core.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@@ -25,7 +25,7 @@ class ThreadTableTest_recents {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -70,7 +70,7 @@ class EditMessageSyncProcessorTest {
|
||||
val syncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
||||
.setDestinationUuid(metadata.destinationServiceId.toString())
|
||||
.setDestinationServiceId(metadata.destinationServiceId.toString())
|
||||
.setTimestamp(originalTimestamp)
|
||||
.setExpirationStartTimestamp(originalTimestamp)
|
||||
.setMessage(content.dataMessage)
|
||||
@@ -89,7 +89,7 @@ class EditMessageSyncProcessorTest {
|
||||
val editSyncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
||||
.setDestinationUuid(metadata.destinationServiceId.toString())
|
||||
.setDestinationServiceId(metadata.destinationServiceId.toString())
|
||||
.setTimestamp(editTimestamp)
|
||||
.setExpirationStartTimestamp(editTimestamp)
|
||||
.setEditMessage(
|
||||
|
||||
@@ -46,9 +46,9 @@ abstract class MessageContentProcessorTest {
|
||||
): SignalServiceContentProto {
|
||||
return TestProtos.build {
|
||||
serviceContent(
|
||||
localAddress = address(uuid = harness.self.requireServiceId().uuid()).build(),
|
||||
localAddress = address(uuid = harness.self.requireServiceId().rawUuid).build(),
|
||||
metadata = metadata(
|
||||
address = address(uuid = messageSender.requireServiceId().uuid()).build()
|
||||
address = address(uuid = messageSender.requireServiceId().rawUuid).build()
|
||||
).build()
|
||||
).apply {
|
||||
content = content().apply {
|
||||
|
||||
@@ -92,12 +92,12 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
|
||||
.addAllMembers(
|
||||
listOf(
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(harness.self.requireServiceId().toByteString())
|
||||
.setAciBytes(harness.self.requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build(),
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(sender.requireServiceId().toByteString())
|
||||
.setAciBytes(sender.requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
|
||||
@@ -14,8 +14,8 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
@@ -39,14 +39,13 @@ class ContactRecordProcessorTest {
|
||||
setStorageId(originalId, STORAGE_ID_A)
|
||||
|
||||
val remote1 = buildRecord(STORAGE_ID_B) {
|
||||
setServiceId(ACI_A.toString())
|
||||
setAci(ACI_A.toString())
|
||||
setUnregisteredAtTimestamp(100)
|
||||
}
|
||||
|
||||
val remote2 = buildRecord(STORAGE_ID_C) {
|
||||
setServiceId(PNI_A.toString())
|
||||
setServicePni(PNI_A.toString())
|
||||
setServiceE164(E164_A)
|
||||
setPni(PNI_A.toString())
|
||||
setE164(E164_A)
|
||||
}
|
||||
|
||||
// WHEN
|
||||
@@ -54,10 +53,10 @@ class ContactRecordProcessorTest {
|
||||
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
|
||||
|
||||
// THEN
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
assertEquals(originalId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
@@ -71,14 +70,14 @@ class ContactRecordProcessorTest {
|
||||
setStorageId(originalId, STORAGE_ID_A)
|
||||
|
||||
val remote1 = buildRecord(STORAGE_ID_B) {
|
||||
setServiceId(ACI_A.toString())
|
||||
setAci(ACI_A.toString())
|
||||
setUnregisteredAtTimestamp(0)
|
||||
}
|
||||
|
||||
val remote2 = buildRecord(STORAGE_ID_C) {
|
||||
setServiceId(PNI_A.toString())
|
||||
setServicePni(PNI_A.toString())
|
||||
setServiceE164(E164_A)
|
||||
setAci(PNI_A.toString())
|
||||
setPni(PNI_A.toString())
|
||||
setE164(E164_A)
|
||||
}
|
||||
|
||||
// WHEN
|
||||
@@ -86,7 +85,7 @@ class ContactRecordProcessorTest {
|
||||
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
|
||||
|
||||
// THEN
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
|
||||
|
||||
private val aliceSenderCertificate = FakeClientHelpers.createCertificateFor(
|
||||
trustRoot = trustRoot,
|
||||
uuid = serviceId.uuid(),
|
||||
uuid = serviceId.rawUuid,
|
||||
e164 = e164,
|
||||
deviceId = 1,
|
||||
identityKey = SignalStore.account().aciIdentityKey.publicKey.publicKey,
|
||||
|
||||
@@ -50,7 +50,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
private val serviceAddress = SignalServiceAddress(serviceId, e164)
|
||||
private val registrationId = KeyHelper.generateRegistrationId(false)
|
||||
private val aciStore = BobSignalServiceAccountDataStore(registrationId, identityKeyPair)
|
||||
private val senderCertificate = FakeClientHelpers.createCertificateFor(trustRoot, serviceId.uuid(), e164, 1, identityKeyPair.publicKey.publicKey, 31337)
|
||||
private val senderCertificate = FakeClientHelpers.createCertificateFor(trustRoot, serviceId.rawUuid, e164, 1, identityKeyPair.publicKey.publicKey, 31337)
|
||||
private val sessionLock = object : SignalSessionLock {
|
||||
private val lock = ReentrantLock()
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ object FakeClientHelpers {
|
||||
.setSourceDevice(1)
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 1)
|
||||
.setDestinationUuid(destination.toString())
|
||||
.setDestinationServiceId(destination.toString())
|
||||
.setServerGuid(UUID.randomUUID().toString())
|
||||
.setContent(Base64.decode(this.content).toProtoByteString())
|
||||
.setUrgent(true)
|
||||
|
||||
@@ -8,16 +8,16 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Helper methods for creating groups for message processing tests et al.
|
||||
*/
|
||||
object GroupTestingUtils {
|
||||
fun member(serviceId: ServiceId, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
|
||||
fun member(aci: ACI, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(serviceId.toByteString())
|
||||
.setAciBytes(aci.toByteString())
|
||||
.setJoinedAtRevision(revision)
|
||||
.setRole(role)
|
||||
.build()
|
||||
@@ -43,7 +43,7 @@ object GroupTestingUtils {
|
||||
}
|
||||
|
||||
fun Recipient.asMember(): DecryptedMember {
|
||||
return member(serviceId = requireServiceId())
|
||||
return member(aci = requireAci())
|
||||
}
|
||||
|
||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId)
|
||||
|
||||
@@ -105,7 +105,7 @@ object MessageContentFuzzer {
|
||||
addAllUnidentifiedStatus(
|
||||
deliveredTo.map {
|
||||
SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder().buildWith {
|
||||
destinationUuid = Recipient.resolved(it).requireServiceId().toString()
|
||||
destinationServiceId = Recipient.resolved(it).requireServiceId().toString()
|
||||
unidentified = true
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,7 @@ object MessageContentFuzzer {
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.newBuilder().buildWith {
|
||||
id = quoted.envelope.timestamp
|
||||
authorUuid = quoted.metadata.sourceServiceId.toString()
|
||||
authorAci = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage.body
|
||||
addAllAttachments(quoted.content.dataMessage.attachmentsList)
|
||||
addAllBodyRanges(quoted.content.dataMessage.bodyRangesList)
|
||||
@@ -147,7 +147,7 @@ object MessageContentFuzzer {
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.newBuilder().buildWith {
|
||||
id = random.nextLong(quoted.envelope.timestamp - 1000000, quoted.envelope.timestamp)
|
||||
authorUuid = quoted.metadata.sourceServiceId.toString()
|
||||
authorAci = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage.body
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ object MessageContentFuzzer {
|
||||
reaction = DataMessage.Reaction.newBuilder().buildWith {
|
||||
emoji = emojis.random(random)
|
||||
remove = false
|
||||
targetAuthorUuid = reactTo.metadata.sourceServiceId.toString()
|
||||
targetAuthorAci = reactTo.metadata.sourceServiceId.toString()
|
||||
targetSentTimestamp = reactTo.envelope.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.registration.VerifyResponse
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
|
||||
@@ -4,8 +4,8 @@ import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ class SignalDatabaseRule(
|
||||
|
||||
private fun deleteAllThreads() {
|
||||
if (deleteAllThreadsOnEachRun) {
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
SignalDatabase.threads.clearForTests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.testing
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
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
|
||||
@@ -17,7 +17,7 @@ class TestProtos private constructor() {
|
||||
uuid: UUID = UUID.randomUUID()
|
||||
): AddressProto.Builder {
|
||||
return AddressProto.newBuilder()
|
||||
.setUuid(ServiceId.from(uuid).toByteString())
|
||||
.setUuid(ACI.from(uuid).toByteString())
|
||||
}
|
||||
|
||||
fun metadata(
|
||||
@@ -41,7 +41,7 @@ class TestProtos private constructor() {
|
||||
authorUuid: String = UUID.randomUUID().toString()
|
||||
): DataMessage.StoryContext.Builder {
|
||||
return DataMessage.StoryContext.newBuilder()
|
||||
.setAuthorUuid(authorUuid)
|
||||
.setAuthorAci(authorUuid)
|
||||
.setSentTimestamp(sentTimestamp)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import org.signal.benchmark.setup.TestMessages
|
||||
import org.signal.benchmark.setup.TestUsers
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class BenchmarkSetupActivity : BaseActivity() {
|
||||
@@ -53,13 +51,6 @@ class BenchmarkSetupActivity : BaseActivity() {
|
||||
TestMessages.insertOutgoingTextMessage(other = recipient, body = "Test message $i", timestamp = generator.nextTimestamp())
|
||||
}
|
||||
|
||||
val voiceMessageId = TestMessages.insertIncomingVoiceMessage(other = recipient, timestamp = generator.nextTimestamp())
|
||||
val mmsRecord = SignalDatabase.messages.getMessageRecord(voiceMessageId) as MediaMmsMessageRecord
|
||||
TestMessages.insertOutgoingImageMessage(other = recipient, body = "test", 2, generator.nextTimestamp())
|
||||
TestMessages.insertIncomingTextMessage(other = recipient, "reply to the test message", generator.nextTimestamp())
|
||||
TestMessages.insertIncomingQuoteTextMessage(other = recipient, quote = QuoteModel(mmsRecord.timestamp, recipient.id, "Fake voice message text", false, mmsRecord.slideDeck.asAttachments(), null, QuoteModel.Type.NORMAL, null), body = "Here is a cool quote", timestamp = generator.nextTimestamp())
|
||||
TestMessages.insertOutgoingTextMessage(other = recipient, body = "longaweorijoaijwerijoiajwer", timestamp = generator.nextTimestamp())
|
||||
|
||||
SignalDatabase.threads.update(SignalDatabase.threads.getOrCreateThreadIdFor(recipient = recipient), true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.account.PreKeyUpload
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
import org.signal.benchmark.DummyAccountManagerFactory
|
||||
import org.signal.core.util.concurrent.safeBlockingGet
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||
@@ -22,13 +23,13 @@ import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponse
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.util.UUID
|
||||
|
||||
object TestUsers {
|
||||
|
||||
private var generatedOthers: Int = 0
|
||||
@@ -43,6 +44,9 @@ object TestUsers {
|
||||
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
||||
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
||||
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
|
||||
val registrationRepository = RegistrationRepository(application)
|
||||
val registrationData = RegistrationData(
|
||||
code = "123123",
|
||||
@@ -50,19 +54,27 @@ object TestUsers {
|
||||
password = Util.getSecret(18),
|
||||
registrationId = registrationRepository.registrationId,
|
||||
profileKey = registrationRepository.getProfileKey("+15555550101"),
|
||||
aciPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.ACI),
|
||||
pniPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.PNI),
|
||||
fcmToken = "fcm-token",
|
||||
pniRegistrationId = registrationRepository.pniRegistrationId,
|
||||
recoveryPassword = "asdfasdfasdfasdf"
|
||||
)
|
||||
val verifyResponse = VerifyResponse(VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false), null, null)
|
||||
|
||||
val verifyResponse = VerifyResponse(
|
||||
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
|
||||
masterKey = null,
|
||||
pin = null,
|
||||
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
|
||||
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
|
||||
)
|
||||
|
||||
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
|
||||
|
||||
val response: ServiceResponse<VerifyResponse> = registrationRepository.registerAccount(
|
||||
registrationData,
|
||||
verifyResponse,
|
||||
false
|
||||
).blockingGet()
|
||||
).safeBlockingGet()
|
||||
|
||||
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
||||
|
||||
SignalStore.svr().optOut()
|
||||
|
||||
@@ -630,22 +630,11 @@
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".conversation.ConversationActivity"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.thoughtcrime.securesms.MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".conversation.BubbleConversationActivity"
|
||||
android:theme="@style/Signal.DayNight"
|
||||
android:allowEmbedded="true"
|
||||
android:resizeableActivity="true"
|
||||
android:exported="false"/>
|
||||
android:theme="@style/Signal.DayNight"
|
||||
android:allowEmbedded="true"
|
||||
android:resizeableActivity="true"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".conversation.ConversationPopupActivity"
|
||||
android:windowSoftInputMode="stateVisible"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package androidx.recyclerview.widget
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.logging.Log
|
||||
|
||||
/**
|
||||
* Variation of a vertical, reversed [LinearLayoutManager] that makes specific assumptions in how it will
|
||||
* be used by Conversation view to support easier scrolling to the initial start position.
|
||||
*
|
||||
* Primarily, it assumes that an initial scroll to position call will always happen and that the implementation
|
||||
* of [LinearLayoutManager] remains unchanged with respect to how it assigns [mPendingScrollPosition] and
|
||||
* [mPendingScrollPositionOffset] in [LinearLayoutManager.scrollToPositionWithOffset] and how it always clears
|
||||
* the pending state variables in every call to [LinearLayoutManager.onLayoutCompleted].
|
||||
*
|
||||
* The assumptions are necessary to force the requested scroll position/layout to occur even if the request
|
||||
* happens prior to the data source populating the recycler view/adapter.
|
||||
*/
|
||||
class ConversationLayoutManager(context: Context) : LinearLayoutManager(context, RecyclerView.VERTICAL, true) {
|
||||
|
||||
private var afterScroll: (() -> Unit)? = null
|
||||
|
||||
override fun supportsPredictiveItemAnimations(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the desired position and be notified when the layout manager has completed the request
|
||||
* via [afterScroll] callback.
|
||||
*/
|
||||
fun scrollToPositionWithOffset(position: Int, offset: Int, afterScroll: () -> Unit) {
|
||||
this.afterScroll = afterScroll
|
||||
super.scrollToPositionWithOffset(position, offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* If a scroll to position request is made and a layout pass occurs prior to the list being populated with via the data source,
|
||||
* the base implementation clears the request as if it was never made.
|
||||
*
|
||||
* This override will capture the pending scroll position and offset, determine if the scroll request was satisfied, and
|
||||
* re-request the scroll to position to force another attempt if not satisfied.
|
||||
*
|
||||
* A pending scroll request will be re-requested if the pending scroll position is outside the bounds of the current known size of
|
||||
* items in the list.
|
||||
*/
|
||||
override fun onLayoutCompleted(state: RecyclerView.State?) {
|
||||
val pendingScrollPosition = mPendingScrollPosition
|
||||
val pendingScrollOffset = mPendingScrollPositionOffset
|
||||
|
||||
val reRequestPendingPosition = pendingScrollPosition >= (state?.mItemCount ?: 0)
|
||||
|
||||
// Base implementation always clears mPendingScrollPosition+mPendingScrollPositionOffset
|
||||
super.onLayoutCompleted(state)
|
||||
|
||||
// Re-request scroll to position request if necessary thus forcing mPendingScrollPosition+mPendingScrollPositionOffset to be re-assigned
|
||||
if (reRequestPendingPosition) {
|
||||
Log.d(TAG, "Re-requesting pending scroll position: $pendingScrollPosition offset: $pendingScrollOffset")
|
||||
if (pendingScrollOffset != INVALID_OFFSET) {
|
||||
scrollToPositionWithOffset(pendingScrollPosition, pendingScrollOffset)
|
||||
} else {
|
||||
scrollToPosition(pendingScrollPosition)
|
||||
}
|
||||
} else {
|
||||
afterScroll?.invoke()
|
||||
afterScroll = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ConversationLayoutManager::class.java)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.insights.InsightsOptOut;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
@@ -30,7 +29,6 @@ public final class AppInitialization {
|
||||
public static void onFirstEverAppLaunch(@NonNull Context context) {
|
||||
Log.i(TAG, "onFirstEverAppLaunch()");
|
||||
|
||||
InsightsOptOut.userRequestedOptOut(context);
|
||||
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
|
||||
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
||||
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
|
||||
@@ -71,7 +69,6 @@ public final class AppInitialization {
|
||||
public static void onRepairFirstEverAppLaunch(@NonNull Context context) {
|
||||
Log.w(TAG, "onRepairFirstEverAppLaunch()");
|
||||
|
||||
InsightsOptOut.userRequestedOptOut(context);
|
||||
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
|
||||
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
|
||||
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.gcm.FcmFetchManager;
|
||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
|
||||
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
|
||||
@@ -230,6 +231,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
ApplicationDependencies.getDeadlockDetector().start();
|
||||
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
|
||||
FcmFetchManager.onForeground(this);
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
|
||||
@@ -254,13 +254,8 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
for (SelectedContact contact : contacts) {
|
||||
RecipientId recipientId = contact.getOrCreateRecipientId(context);
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
int subscriptionId = recipient.getDefaultSubscriptionId().orElse(-1);
|
||||
|
||||
MessageSender.send(context, OutgoingMessage.sms(recipient, message, subscriptionId), -1L, MessageSender.SendType.SMS, null, null);
|
||||
|
||||
if (recipient.getContactUri() != null) {
|
||||
SignalDatabase.recipients().setHasSentInvite(recipient.getId());
|
||||
}
|
||||
MessageSender.send(context, OutgoingMessage.sms(recipient, message), -1L, MessageSender.SendType.SMS, null, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -15,12 +15,14 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
|
||||
import org.thoughtcrime.securesms.notifications.SlowNotificationHeuristics;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
@@ -136,6 +138,10 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
}
|
||||
|
||||
updateTabVisibility();
|
||||
|
||||
if (SlowNotificationHeuristics.shouldPromptUserForLogs()) {
|
||||
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,7 +11,6 @@ import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
@@ -78,10 +77,6 @@ public class MainNavigator {
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
public void goToInsights() {
|
||||
InsightsLauncher.showInsightsDashboard(activity.getSupportFragmentManager());
|
||||
}
|
||||
|
||||
private @NonNull FragmentManager getFragmentManager() {
|
||||
return activity.getSupportFragmentManager();
|
||||
}
|
||||
|
||||
@@ -371,6 +371,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
.setPositiveButton(R.string.NewConversationActivity__remove,
|
||||
(dialog, which) -> {
|
||||
disposables.add(viewModel.hideContact(recipient).subscribe(() -> {
|
||||
onRefresh();
|
||||
displaySnackbar(R.string.NewConversationActivity__s_has_been_removed, recipient.getDisplayName(this));
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter;
|
||||
import androidx.window.layout.DisplayFeature;
|
||||
@@ -50,6 +51,7 @@ import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
@@ -61,6 +63,10 @@ import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
|
||||
import org.thoughtcrime.securesms.components.webrtc.InCallStatus;
|
||||
import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsBottomSheet;
|
||||
import org.thoughtcrime.securesms.components.webrtc.PendingParticipantsView;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioDevice;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
|
||||
@@ -76,8 +82,10 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
|
||||
import org.thoughtcrime.securesms.service.webrtc.CallLinkDisconnectReason;
|
||||
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
@@ -93,8 +101,10 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
@@ -109,7 +119,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
/**
|
||||
* ANSWER the call via voice-only.
|
||||
*/
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
|
||||
|
||||
/**
|
||||
* ANSWER the call via video.
|
||||
@@ -120,6 +130,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
|
||||
public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN";
|
||||
public static final String EXTRA_STARTED_FROM_CALL_LINK = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
|
||||
|
||||
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
|
||||
private CallStateUpdatePopupWindow callStateUpdatePopupWindow;
|
||||
@@ -136,6 +147,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private WindowInfoTrackerCallbackAdapter windowInfoTrackerCallbackAdapter;
|
||||
private ThrottledDebouncer requestNewSizesThrottle;
|
||||
private PictureInPictureParams.Builder pipBuilderParams;
|
||||
private LifecycleDisposable lifecycleDisposable;
|
||||
private long lastCallLinkDisconnectDialogShowTime;
|
||||
|
||||
private Disposable ephemeralStateDisposable = Disposable.empty();
|
||||
|
||||
@@ -149,6 +162,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Log.i(TAG, "onCreate(" + getIntent().getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")");
|
||||
|
||||
lifecycleDisposable = new LifecycleDisposable();
|
||||
lifecycleDisposable.bindTo(this);
|
||||
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -188,6 +205,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
windowInfoTrackerCallbackAdapter.addWindowLayoutInfoListener(this, SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
|
||||
|
||||
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
|
||||
|
||||
initializePendingParticipantFragmentListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -239,7 +258,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
super.onPause();
|
||||
|
||||
if (!viewModel.isCallStarting()) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
finish();
|
||||
}
|
||||
@@ -259,7 +278,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
if (!viewModel.isCallStarting()) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
}
|
||||
@@ -334,6 +353,54 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}
|
||||
|
||||
private void initializePendingParticipantFragmentListener() {
|
||||
if (!FeatureFlags.adHocCalling()) {
|
||||
return;
|
||||
}
|
||||
|
||||
getSupportFragmentManager().setFragmentResultListener(
|
||||
PendingParticipantsBottomSheet.REQUEST_KEY,
|
||||
this,
|
||||
(requestKey, result) -> {
|
||||
PendingParticipantsBottomSheet.Action action = PendingParticipantsBottomSheet.getAction(result);
|
||||
List<RecipientId> recipientIds = viewModel.getPendingParticipantsSnapshot()
|
||||
.getUnresolvedPendingParticipants()
|
||||
.stream()
|
||||
.map(r -> r.getRecipient().getId())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
switch (action) {
|
||||
case NONE:
|
||||
break;
|
||||
case APPROVE_ALL:
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getResources().getQuantityString(R.plurals.WebRtcCallActivity__approve_d_requests, recipientIds.size(), recipientIds.size()))
|
||||
.setMessage(getResources().getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_be_added_to_the_call, recipientIds.size(), recipientIds.size()))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.WebRtcCallActivity__approve_all, (dialog, which) -> {
|
||||
for (RecipientId id : recipientIds) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
break;
|
||||
case DENY_ALL:
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getResources().getQuantityString(R.plurals.WebRtcCallActivity__deny_d_requests, recipientIds.size(), recipientIds.size()))
|
||||
.setMessage(getResources().getQuantityString(R.plurals.WebRtcCallActivity__d_people_will_be_added_to_the_call, recipientIds.size(), recipientIds.size()))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.WebRtcCallActivity__deny_all, (dialog, which) -> {
|
||||
for (RecipientId id : recipientIds) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void initializeScreenshotSecurity() {
|
||||
if (TextSecurePreferences.isScreenSecurityEnabled(this)) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||
@@ -363,21 +430,23 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
|
||||
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
|
||||
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
|
||||
lifecycleDisposable.add(viewModel.getInCallstatus().subscribe(this::handleInCallStatus));
|
||||
|
||||
boolean isStartedFromCallLink = getIntent().getBooleanExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, false);
|
||||
LiveDataUtil.combineLatest(LiveDataReactiveStreams.fromPublisher(viewModel.getCallParticipantsState().toFlowable(BackpressureStrategy.LATEST)),
|
||||
viewModel.getOrientationAndLandscapeEnabled(),
|
||||
viewModel.getEphemeralState(),
|
||||
(s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
|
||||
(s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second, isStartedFromCallLink))
|
||||
.observe(this, p -> callScreen.updateCallParticipants(p));
|
||||
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
|
||||
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
|
||||
viewModel.getGroupMembersChanged().observe(this, unused -> updateGroupMembersForGroupCall());
|
||||
viewModel.getGroupMemberCount().observe(this, this::handleGroupMemberCountChange);
|
||||
viewModel.shouldShowSpeakerHint().observe(this, this::updateSpeakerHint);
|
||||
lifecycleDisposable.add(viewModel.shouldShowSpeakerHint().subscribe(this::updateSpeakerHint));
|
||||
|
||||
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null) {
|
||||
if (state.needsNewRequestSizes()) {
|
||||
requestNewSizesThrottle.publish(() -> ApplicationDependencies.getSignalCallManager().updateRenderedResolutions());
|
||||
@@ -397,6 +466,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
viewModel.setIsLandscapeEnabled(info.isInPictureInPictureMode());
|
||||
});
|
||||
|
||||
callScreen.setPendingParticipantsViewListener(new PendingParticipantsViewListener());
|
||||
Disposable disposable = viewModel.getPendingParticipants()
|
||||
.subscribe(callScreen::updatePendingParticipantsList);
|
||||
|
||||
lifecycleDisposable.add(disposable);
|
||||
}
|
||||
|
||||
private void initializePictureInPictureParams() {
|
||||
@@ -458,14 +533,35 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCallTime(long callTime) {
|
||||
EllapsedTimeFormatter ellapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(callTime);
|
||||
private void handleInCallStatus(@NonNull InCallStatus inCallStatus) {
|
||||
if (inCallStatus instanceof InCallStatus.ElapsedTime) {
|
||||
|
||||
if (ellapsedTimeFormatter == null) {
|
||||
return;
|
||||
EllapsedTimeFormatter ellapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(((InCallStatus.ElapsedTime) inCallStatus).getElapsedTime());
|
||||
|
||||
if (ellapsedTimeFormatter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString()));
|
||||
} else if (inCallStatus instanceof InCallStatus.PendingCallLinkUsers) {
|
||||
int waiting = ((InCallStatus.PendingCallLinkUsers) inCallStatus).getPendingUserCount();
|
||||
|
||||
callScreen.setStatus(getResources().getQuantityString(
|
||||
R.plurals.WebRtcCallActivity__d_people_waiting,
|
||||
waiting,
|
||||
waiting
|
||||
));
|
||||
} else if (inCallStatus instanceof InCallStatus.JoinedCallLinkUsers) {
|
||||
int joined = ((InCallStatus.JoinedCallLinkUsers) inCallStatus).getJoinedUserCount();
|
||||
|
||||
callScreen.setStatus(getResources().getQuantityString(
|
||||
R.plurals.WebRtcCallActivity__d_people,
|
||||
joined,
|
||||
joined
|
||||
));
|
||||
}else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString()));
|
||||
}
|
||||
|
||||
private void handleSetAudioHandset() {
|
||||
@@ -672,7 +768,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@Override
|
||||
public void onSendAnywayAfterSafetyNumberChange(@NonNull List<RecipientId> changedRecipients) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
|
||||
if (state == null) {
|
||||
return;
|
||||
@@ -686,11 +782,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageResentAfterSafetyNumberChange() { }
|
||||
public void onMessageResentAfterSafetyNumberChange() {}
|
||||
|
||||
@Override
|
||||
public void onCanceled() {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null && state.getGroupCallState().isNotIdle()) {
|
||||
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
@@ -758,6 +854,18 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
handleUntrustedIdentity(event); break;
|
||||
}
|
||||
|
||||
if (event.getCallLinkDisconnectReason() != null && event.getCallLinkDisconnectReason().getPostedAt() > lastCallLinkDisconnectDialogShowTime) {
|
||||
lastCallLinkDisconnectDialogShowTime = System.currentTimeMillis();
|
||||
|
||||
if (event.getCallLinkDisconnectReason() instanceof CallLinkDisconnectReason.RemovedFromCall) {
|
||||
displayRemovedFromCallLinkDialog();
|
||||
} else if (event.getCallLinkDisconnectReason() instanceof CallLinkDisconnectReason.DeniedRequestToJoinCall) {
|
||||
displayDeniedRequestToJoinCallLinkDialog();
|
||||
} else {
|
||||
throw new AssertionError("Unexpected reason: " + event.getCallLinkDisconnectReason());
|
||||
}
|
||||
}
|
||||
|
||||
boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
|
||||
|
||||
viewModel.updateFromWebRtcViewModel(event, enableVideo);
|
||||
@@ -779,6 +887,22 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}
|
||||
|
||||
private void displayRemovedFromCallLinkDialog() {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.WebRtcCallActivity__removed_from_call)
|
||||
.setMessage(R.string.WebRtcCallActivity__someone_has_removed_you_from_the_call)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void displayDeniedRequestToJoinCallLinkDialog() {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.WebRtcCallActivity__join_request_denied)
|
||||
.setMessage(R.string.WebRtcCallActivity__your_request_to_join_this_call_has_been_denied)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
|
||||
if (event.getGroupState().isNotIdle()) {
|
||||
callScreen.setStatusFromGroupCallState(event.getGroupState());
|
||||
@@ -833,6 +957,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@Override
|
||||
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
|
||||
maybeDisplaySpeakerphonePopup(audioOutput);
|
||||
switch (audioOutput) {
|
||||
case HANDSET:
|
||||
handleSetAudioHandset();
|
||||
@@ -853,8 +978,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@RequiresApi(31)
|
||||
@Override
|
||||
public void onAudioOutputChanged31(@NonNull Integer audioDeviceInfo) {
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioDeviceInfo));
|
||||
public void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput) {
|
||||
maybeDisplaySpeakerphonePopup(audioOutput.getWebRtcAudioOutput());
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -937,6 +1063,33 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeDisplaySpeakerphonePopup(WebRtcAudioOutput nextOutput) {
|
||||
final WebRtcAudioOutput currentOutput = viewModel.getCurrentAudioOutput();
|
||||
if (currentOutput == WebRtcAudioOutput.SPEAKER && nextOutput != WebRtcAudioOutput.SPEAKER) {
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.SPEAKER_OFF);
|
||||
} else if (currentOutput != WebRtcAudioOutput.SPEAKER && nextOutput == WebRtcAudioOutput.SPEAKER) {
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.SPEAKER_ON);
|
||||
}
|
||||
}
|
||||
|
||||
private class PendingParticipantsViewListener implements PendingParticipantsView.Listener {
|
||||
|
||||
@Override
|
||||
public void onAllowPendingRecipient(@NonNull Recipient pendingRecipient) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejectPendingRecipient(@NonNull Recipient pendingRecipient) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLaunchPendingRequestsSheet() {
|
||||
new PendingParticipantsBottomSheet().show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -113,7 +113,7 @@ public class PointerAttachment extends Attachment {
|
||||
pointer.get().asPointer().getRemoteId().toString(),
|
||||
encodedKey, null,
|
||||
pointer.get().asPointer().getDigest().orElse(null),
|
||||
pointer.get().asPointer().getincrementalDigest().orElse(null),
|
||||
pointer.get().asPointer().getIncrementalDigest().orElse(null),
|
||||
fastPreflightId,
|
||||
pointer.get().asPointer().getVoiceNote(),
|
||||
pointer.get().asPointer().isBorderless(),
|
||||
@@ -139,7 +139,7 @@ public class PointerAttachment extends Attachment {
|
||||
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
||||
null,
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getIncrementalDigest().orElse(null) : null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
@@ -169,7 +169,7 @@ public class PointerAttachment extends Attachment {
|
||||
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
|
||||
null,
|
||||
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null,
|
||||
thumbnail != null ? thumbnail.asPointer().getIncrementalDigest().orElse(null) : null,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -64,11 +64,21 @@ public class AudioRecorder {
|
||||
}
|
||||
|
||||
public @NonNull Single<VoiceNoteDraft> startRecording() {
|
||||
Log.i(TAG, "startRecording()");
|
||||
return startRecording(Build.VERSION.SDK_INT >= 26);
|
||||
}
|
||||
|
||||
public @NonNull Single<VoiceNoteDraft> startRecording(final boolean useMediaCodecWrapper) {
|
||||
Log.i(TAG, "startRecording(" + useMediaCodecWrapper + ")");
|
||||
|
||||
final SingleSubject<VoiceNoteDraft> recordingSingle = SingleSubject.create();
|
||||
startRecordingInternal(useMediaCodecWrapper, recordingSingle);
|
||||
|
||||
return recordingSingle;
|
||||
}
|
||||
|
||||
private void startRecordingInternal(boolean useMediaRecorderWrapper, SingleSubject<VoiceNoteDraft> recordingSingle) {
|
||||
executor.execute(() -> {
|
||||
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
|
||||
Log.i(TAG, "Running startRecording(" + useMediaRecorderWrapper + ") + " + Thread.currentThread().getId());
|
||||
try {
|
||||
if (recorder != null) {
|
||||
recordingSingle.onError(new IllegalStateException("We can only do one recording at a time!"));
|
||||
@@ -82,7 +92,7 @@ public class AudioRecorder {
|
||||
.withMimeType(MediaUtil.AUDIO_AAC)
|
||||
.createForDraftAttachmentAsync(context);
|
||||
|
||||
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
|
||||
recorder = useMediaRecorderWrapper ? new MediaRecorderWrapper() : new AudioCodec();
|
||||
int focusResult = audioFocusManager.requestAudioFocus();
|
||||
if (focusResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||
Log.w(TAG, "Could not gain audio focus. Received result code " + focusResult);
|
||||
@@ -90,13 +100,17 @@ public class AudioRecorder {
|
||||
recorder.start(fds[1]);
|
||||
this.recordingSubject = recordingSingle;
|
||||
} catch (IOException | RuntimeException e) {
|
||||
recordingSingle.onError(e);
|
||||
recorder = null;
|
||||
Log.w(TAG, e);
|
||||
recordingUriFuture = null;
|
||||
recorder = null;
|
||||
audioFocusManager.abandonAudioFocus();
|
||||
if (useMediaRecorderWrapper) {
|
||||
startRecordingInternal(false, recordingSingle);
|
||||
} else {
|
||||
recordingSingle.onError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return recordingSingle;
|
||||
}
|
||||
|
||||
public void stopRecording() {
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.annotation.AnyThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.SingleSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
@@ -29,6 +30,7 @@ object AudioWaveForms {
|
||||
private val TAG = Log.tag(AudioWaveForms::class.java)
|
||||
|
||||
private val cache = ThreadSafeLruCache(200)
|
||||
private val pending = hashMapOf<String, SingleSubject<AudioFileInfo>>()
|
||||
|
||||
@AnyThread
|
||||
@JvmStatic
|
||||
@@ -43,39 +45,47 @@ object AudioWaveForms {
|
||||
val cachedInfo = cache.get(cacheKey)
|
||||
if (cachedInfo != null) {
|
||||
Log.i(TAG, "Loaded wave form from cache $cacheKey")
|
||||
synchronized(pending) {
|
||||
pending.remove(cacheKey)
|
||||
}
|
||||
return Single.just(cachedInfo)
|
||||
}
|
||||
|
||||
val databaseCache = Single.fromCallable {
|
||||
val audioHash = attachment.audioHash
|
||||
return@fromCallable if (audioHash != null) {
|
||||
checkDatabaseCache(cacheKey, audioHash.audioWaveForm)
|
||||
val pendingSubject = synchronized(pending) {
|
||||
if (pending.containsKey(cacheKey)) {
|
||||
Log.i(TAG, "Wave currently generating, returning existing subject")
|
||||
return pending[cacheKey]!!
|
||||
} else {
|
||||
Miss
|
||||
pending[cacheKey] = SingleSubject.create()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
|
||||
val generateWaveForm: Single<CacheCheckResult> = if (attachment is DatabaseAttachment) {
|
||||
Single.fromCallable { generateWaveForm(context, uri, cacheKey, attachment.attachmentId) }
|
||||
} else {
|
||||
Single.fromCallable { generateWaveForm(context, uri, cacheKey) }
|
||||
}.subscribeOn(Schedulers.io())
|
||||
pending[cacheKey]!!
|
||||
}
|
||||
|
||||
return databaseCache
|
||||
.flatMap { r ->
|
||||
if (r is Miss) {
|
||||
generateWaveForm
|
||||
Single.fromCallable { attachment.audioHash?.let { checkDatabaseCache(cacheKey, it.audioWaveForm) } ?: Miss }
|
||||
.flatMap { result ->
|
||||
if (result !is Success) {
|
||||
if (attachment is DatabaseAttachment) {
|
||||
Single.fromCallable { generateWaveForm(context, uri, cacheKey, attachment.attachmentId) }
|
||||
} else {
|
||||
Single.fromCallable { generateWaveForm(context, uri, cacheKey) }
|
||||
}
|
||||
} else {
|
||||
Single.just(r)
|
||||
Single.just(result)
|
||||
}
|
||||
}
|
||||
.map { r ->
|
||||
if (r is Success) {
|
||||
r.audioFileInfo
|
||||
.map { result ->
|
||||
if (result is Success) {
|
||||
result.audioFileInfo
|
||||
} else {
|
||||
throw IOException("Unable to generate wave form")
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribe(pendingSubject)
|
||||
|
||||
return pendingSubject
|
||||
}
|
||||
|
||||
private fun checkDatabaseCache(cacheKey: String, audioWaveForm: AudioWaveFormData): CacheCheckResult {
|
||||
|
||||
@@ -41,7 +41,7 @@ public class MediaRecorderWrapper implements Recorder {
|
||||
recorder.setAudioChannels(CHANNELS);
|
||||
recorder.prepare();
|
||||
recorder.start();
|
||||
} catch (IllegalStateException e) {
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Unable to start recording", e);
|
||||
recorder.release();
|
||||
recorder = null;
|
||||
|
||||
@@ -47,7 +47,7 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper? {
|
||||
return Material3OnScrollHelper(requireActivity(), scrollShadow)
|
||||
return Material3OnScrollHelper(requireActivity(), scrollShadow, viewLifecycleOwner)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import java.net.URLDecoder
|
||||
|
||||
/**
|
||||
@@ -53,6 +54,10 @@ object CallLinks {
|
||||
|
||||
@JvmStatic
|
||||
fun isCallLink(url: String): Boolean {
|
||||
if (FeatureFlags.adHocCalling()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
Log.w(TAG, "Invalid url prefix.")
|
||||
return false
|
||||
@@ -63,6 +68,10 @@ object CallLinks {
|
||||
|
||||
@JvmStatic
|
||||
fun parseUrl(url: String): CallLinkRootKey? {
|
||||
if (!FeatureFlags.adHocCalling()) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
Log.w(TAG, "Invalid url prefix.")
|
||||
return null
|
||||
|
||||
@@ -28,6 +28,7 @@ import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -35,7 +36,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Buttons
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
@@ -49,10 +49,10 @@ import java.time.Instant
|
||||
@Composable
|
||||
private fun SignalCallRowPreview() {
|
||||
val callLink = remember {
|
||||
val credentials = CallLinkCredentials.generate()
|
||||
val credentials = CallLinkCredentials(byteArrayOf(1, 2, 3, 4), byteArrayOf(5, 6, 7, 8))
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = CallLinkRoomId.fromCallLinkRootKey(CallLinkRootKey(credentials.linkKeyBytes)),
|
||||
roomId = CallLinkRoomId.fromBytes(byteArrayOf(1, 3, 5, 7)),
|
||||
credentials = credentials,
|
||||
state = SignalCallLinkState(
|
||||
name = "Call Name",
|
||||
@@ -76,6 +76,14 @@ fun SignalCallRow(
|
||||
onJoinClicked: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val callUrl = if (LocalInspectionMode.current) {
|
||||
"https://signal.call.example.com"
|
||||
} else {
|
||||
remember(callLink.credentials) {
|
||||
callLink.credentials?.let { CallLinks.url(it.linkKeyBytes) } ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@@ -113,7 +121,7 @@ fun SignalCallRow(
|
||||
text = callLink.state.name.ifEmpty { stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__signal_call) }
|
||||
)
|
||||
Text(
|
||||
text = callLink.credentials?.let { CallLinks.url(it.linkKeyBytes) } ?: "",
|
||||
text = callUrl,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.ringrtc.CallLinkState
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.CallLinkUpdateSendJob
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
@@ -58,6 +59,7 @@ class UpdateCallLinkRepository(
|
||||
return { result ->
|
||||
if (result is UpdateCallLinkResult.Success) {
|
||||
SignalDatabase.callLinks.updateCallLinkState(credentials.roomId, result.state)
|
||||
ApplicationDependencies.getJobManager().add(CallLinkUpdateSendJob(credentials.roomId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
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
|
||||
@@ -61,6 +62,7 @@ class CreateCallLinkViewModel(
|
||||
|
||||
fun commitCallLink(): Single<EnsureCallLinkCreatedResult> {
|
||||
return repository.ensureCallLinkCreated(credentials)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun setApproveAllMembers(approveAllMembers: Boolean): Single<UpdateCallLinkResult> {
|
||||
@@ -74,10 +76,12 @@ class CreateCallLinkViewModel(
|
||||
is EnsureCallLinkCreatedResult.Failure -> Single.just(UpdateCallLinkResult.Failure(it.failure.status))
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun toggleApproveAllMembers(): Single<UpdateCallLinkResult> {
|
||||
return setApproveAllMembers(_callLink.value.state.restrictions != Restrictions.ADMIN_APPROVAL)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun setCallName(callName: String): Single<UpdateCallLinkResult> {
|
||||
@@ -91,5 +95,6 @@ class CreateCallLinkViewModel(
|
||||
is EnsureCallLinkCreatedResult.Failure -> Single.just(UpdateCallLinkResult.Failure(it.failure.status))
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,18 +244,20 @@ private fun CallLinkDetails(
|
||||
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp)
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name),
|
||||
onClick = callback::onEditNameClicked
|
||||
)
|
||||
if (state.callLink.credentials?.adminPassBytes != null) {
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name),
|
||||
onClick = callback::onEditNameClicked
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
checked = state.callLink.state.restrictions == Restrictions.ADMIN_APPROVAL,
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__approve_all_members),
|
||||
onCheckChanged = callback::onApproveAllMembersChanged
|
||||
)
|
||||
Rows.ToggleRow(
|
||||
checked = state.callLink.state.restrictions == Restrictions.ADMIN_APPROVAL,
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__approve_all_members),
|
||||
onCheckChanged = callback::onApproveAllMembersChanged
|
||||
)
|
||||
|
||||
Dividers.Default()
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
|
||||
|
||||
@@ -215,7 +215,15 @@ class CallLogAdapter(
|
||||
binding.callInfo.setRelativeDrawables(start = R.drawable.symbol_link_compact_16)
|
||||
binding.callInfo.setText(R.string.CallLogAdapter__call_link)
|
||||
|
||||
TextViewCompat.setCompoundDrawableTintList(
|
||||
binding.callInfo,
|
||||
ColorStateList.valueOf(
|
||||
ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant)
|
||||
)
|
||||
)
|
||||
|
||||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
|
||||
binding.callType.setOnClickListener {
|
||||
onStartVideoCallClicked(model.callLink.recipient)
|
||||
}
|
||||
@@ -306,6 +314,7 @@ class CallLogAdapter(
|
||||
when (model.call.record.type) {
|
||||
CallTable.Type.AUDIO_CALL -> {
|
||||
binding.callType.setImageResource(R.drawable.symbol_phone_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_voice_call)
|
||||
binding.callType.setOnClickListener { onStartAudioCallClicked(model.call.peer) }
|
||||
binding.callType.visible = true
|
||||
binding.groupCallButton.visible = false
|
||||
@@ -313,6 +322,7 @@ class CallLogAdapter(
|
||||
|
||||
CallTable.Type.VIDEO_CALL -> {
|
||||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer) }
|
||||
binding.callType.visible = true
|
||||
binding.groupCallButton.visible = false
|
||||
@@ -320,6 +330,7 @@ class CallLogAdapter(
|
||||
|
||||
CallTable.Type.GROUP_CALL, CallTable.Type.AD_HOC_CALL -> {
|
||||
binding.callType.setImageResource(R.drawable.symbol_video_24)
|
||||
binding.callType.contentDescription = context.getString(R.string.CallLogAdapter__start_a_video_call)
|
||||
binding.callType.setOnClickListener { onStartVideoCallClicked(model.call.peer) }
|
||||
binding.groupCallButton.setOnClickListener { onStartVideoCallClicked(model.call.peer) }
|
||||
|
||||
@@ -354,7 +365,7 @@ class CallLogAdapter(
|
||||
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16
|
||||
MessageTypes.GROUP_CALL_TYPE -> when {
|
||||
call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16
|
||||
call.event == CallTable.Event.MISSED -> R.drawable.symbol_missed_incoming_24
|
||||
call.event == CallTable.Event.MISSED -> R.drawable.symbol_missed_incoming_compact_16
|
||||
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16
|
||||
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16
|
||||
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16
|
||||
|
||||
@@ -5,11 +5,14 @@ import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.CallLinkPeekJob
|
||||
import org.thoughtcrime.securesms.jobs.CallLogEventSendJob
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
|
||||
|
||||
@@ -80,6 +83,23 @@ class CallLogRepository(
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all call events / unowned links and enqueue clear history job, and then
|
||||
* emit a clear history message.
|
||||
*/
|
||||
fun deleteAllCallLogsOnOrBeforeNow(): Single<Int> {
|
||||
return Single.fromCallable {
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(now)
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(now)
|
||||
ApplicationDependencies.getJobManager().add(CallLogEventSendJob.forClearHistory(now))
|
||||
}
|
||||
|
||||
SignalDatabase.callLinks.getAllAdminCallLinksExcept(emptySet())
|
||||
}.flatMap(this::revokeAndCollectResults).map { -1 }.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected call links. We DELETE those links we don't have admin keys for,
|
||||
* and revoke the ones we *do* have admin keys for. We then perform a cleanup step on
|
||||
@@ -93,19 +113,7 @@ class CallLogRepository(
|
||||
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinks(allCallLinkIds)
|
||||
SignalDatabase.callLinks.getAdminCallLinks(allCallLinkIds)
|
||||
}.flatMap { callLinksToRevoke ->
|
||||
Single.merge(
|
||||
callLinksToRevoke.map {
|
||||
updateCallLinkRepository.revokeCallLink(it.credentials!!)
|
||||
}
|
||||
).reduce(0) { acc, current ->
|
||||
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
|
||||
}
|
||||
}.doOnTerminate {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.doOnDispose {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}.flatMap(this::revokeAndCollectResults).subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,19 +129,21 @@ class CallLogRepository(
|
||||
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
|
||||
SignalDatabase.callLinks.deleteAllNonAdminCallLinksExcept(allCallLinkIds)
|
||||
SignalDatabase.callLinks.getAllAdminCallLinksExcept(allCallLinkIds)
|
||||
}.flatMap { callLinksToRevoke ->
|
||||
Single.merge(
|
||||
callLinksToRevoke.map {
|
||||
updateCallLinkRepository.revokeCallLink(it.credentials!!)
|
||||
}
|
||||
).reduce(0) { acc, current ->
|
||||
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
|
||||
}.flatMap(this::revokeAndCollectResults).subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun revokeAndCollectResults(callLinksToRevoke: Set<CallLinkTable.CallLink>): Single<Int> {
|
||||
return Single.merge(
|
||||
callLinksToRevoke.map {
|
||||
updateCallLinkRepository.revokeCallLink(it.credentials!!)
|
||||
}
|
||||
).reduce(0) { acc, current ->
|
||||
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
|
||||
}.doOnTerminate {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.doOnDispose {
|
||||
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
fun peekCallLinks(): Completable {
|
||||
|
||||
@@ -93,7 +93,7 @@ sealed class CallLogRow {
|
||||
return FULL
|
||||
}
|
||||
|
||||
if (groupCallUpdateDetails.inCallUuidsList.contains(Recipient.self().requireServiceId().uuid().toString())) {
|
||||
if (groupCallUpdateDetails.inCallUuidsList.contains(Recipient.self().requireAci().rawUuid.toString())) {
|
||||
return LOCAL_USER_JOINED
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,17 @@ package org.thoughtcrime.securesms.calls.log
|
||||
/**
|
||||
* Selection state object for call logs.
|
||||
*/
|
||||
sealed class CallLogSelectionState {
|
||||
abstract fun contains(callId: CallLogRow.Id): Boolean
|
||||
abstract fun isNotEmpty(totalCount: Int): Boolean
|
||||
sealed interface CallLogSelectionState {
|
||||
fun contains(callId: CallLogRow.Id): Boolean
|
||||
fun isNotEmpty(totalCount: Int): Boolean
|
||||
|
||||
abstract fun count(totalCount: Int): Int
|
||||
fun count(totalCount: Int): Int
|
||||
|
||||
abstract fun selected(): Set<CallLogRow.Id>
|
||||
fun selected(): Set<CallLogRow.Id>
|
||||
fun isExclusionary(): Boolean = this is Excludes
|
||||
|
||||
protected abstract fun select(callId: CallLogRow.Id): CallLogSelectionState
|
||||
protected abstract fun deselect(callId: CallLogRow.Id): CallLogSelectionState
|
||||
fun select(callId: CallLogRow.Id): CallLogSelectionState
|
||||
fun deselect(callId: CallLogRow.Id): CallLogSelectionState
|
||||
|
||||
fun toggle(callId: CallLogRow.Id): CallLogSelectionState {
|
||||
return if (contains(callId)) {
|
||||
@@ -26,7 +26,7 @@ sealed class CallLogSelectionState {
|
||||
/**
|
||||
* Includes contains an opt-in list of call logs.
|
||||
*/
|
||||
data class Includes(private val includes: Set<CallLogRow.Id>) : CallLogSelectionState() {
|
||||
data class Includes(private val includes: Set<CallLogRow.Id>) : CallLogSelectionState {
|
||||
override fun contains(callId: CallLogRow.Id): Boolean {
|
||||
return includes.contains(callId)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ sealed class CallLogSelectionState {
|
||||
/**
|
||||
* Excludes contains an opt-out list of call logs.
|
||||
*/
|
||||
data class Excludes(private val excluded: Set<CallLogRow.Id>) : CallLogSelectionState() {
|
||||
data class Excludes(private val excluded: Set<CallLogRow.Id>) : CallLogSelectionState {
|
||||
override fun contains(callId: CallLogRow.Id): Boolean = !excluded.contains(callId)
|
||||
override fun isNotEmpty(totalCount: Int): Boolean = excluded.size < totalCount
|
||||
|
||||
@@ -74,8 +74,10 @@ sealed class CallLogSelectionState {
|
||||
override fun selected(): Set<CallLogRow.Id> = excluded
|
||||
}
|
||||
|
||||
object All : CallLogSelectionState by Excludes(emptySet())
|
||||
|
||||
companion object {
|
||||
fun empty(): CallLogSelectionState = Includes(emptySet())
|
||||
fun selectAll(): CallLogSelectionState = Excludes(emptySet())
|
||||
fun selectAll(): CallLogSelectionState = All
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,14 +35,21 @@ class CallLogStagedDeletion(
|
||||
.map { it.roomId }
|
||||
.toSet()
|
||||
|
||||
return if (stateSnapshot.isExclusionary()) {
|
||||
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).andThen(
|
||||
repository.deleteAllCallLinksExcept(callRowIds, callLinkIds)
|
||||
)
|
||||
} else {
|
||||
repository.deleteSelectedCallLogs(callRowIds).andThen(
|
||||
repository.deleteSelectedCallLinks(callRowIds, callLinkIds)
|
||||
)
|
||||
return when {
|
||||
stateSnapshot is CallLogSelectionState.All && filter == CallLogFilter.ALL -> {
|
||||
repository.deleteAllCallLogsOnOrBeforeNow()
|
||||
}
|
||||
stateSnapshot is CallLogSelectionState.Excludes || stateSnapshot is CallLogSelectionState.All -> {
|
||||
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).andThen(
|
||||
repository.deleteAllCallLinksExcept(callRowIds, callLinkIds)
|
||||
)
|
||||
}
|
||||
stateSnapshot is CallLogSelectionState.Includes -> {
|
||||
repository.deleteSelectedCallLogs(callRowIds).andThen(
|
||||
repository.deleteSelectedCallLinks(callRowIds, callLinkIds)
|
||||
)
|
||||
}
|
||||
else -> error("Unhandled state $stateSnapshot $filter")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,6 @@ public final class AudioView extends FrameLayout {
|
||||
final boolean showControls,
|
||||
final boolean forceHideDuration)
|
||||
{
|
||||
this.disposable.dispose();
|
||||
this.callbacks = callbacks;
|
||||
|
||||
if (duration != null) {
|
||||
@@ -213,25 +212,26 @@ public final class AudioView extends FrameLayout {
|
||||
showPlayButton();
|
||||
}
|
||||
|
||||
this.audioSlide = audio;
|
||||
|
||||
if (seekBar instanceof WaveFormSeekBarView) {
|
||||
WaveFormSeekBarView waveFormView = (WaveFormSeekBarView) seekBar;
|
||||
waveFormView.setColors(waveFormPlayedBarsColor, waveFormUnplayedBarsColor, waveFormThumbTint);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 23) {
|
||||
disposable = AudioWaveForms.getWaveForm(getContext(), audioSlide.asAttachment())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
data -> {
|
||||
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
|
||||
updateProgress(0, 0);
|
||||
if (!forceHideDuration && duration != null) {
|
||||
duration.setVisibility(VISIBLE);
|
||||
}
|
||||
waveFormView.setWaveData(data.getWaveForm());
|
||||
},
|
||||
t -> waveFormView.setWaveMode(false)
|
||||
);
|
||||
if (audioSlide == null || !Objects.equals(audioSlide.getUri(), audio.getUri())) {
|
||||
disposable.dispose();
|
||||
disposable = AudioWaveForms.getWaveForm(getContext(), audio.asAttachment())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
data -> {
|
||||
durationMillis = data.getDuration(TimeUnit.MILLISECONDS);
|
||||
updateProgress(0, 0);
|
||||
if (!forceHideDuration && duration != null) {
|
||||
duration.setVisibility(VISIBLE);
|
||||
}
|
||||
waveFormView.setWaveData(data.getWaveForm());
|
||||
},
|
||||
t -> waveFormView.setWaveMode(false)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
waveFormView.setWaveMode(false);
|
||||
if (duration != null) {
|
||||
@@ -243,6 +243,8 @@ public final class AudioView extends FrameLayout {
|
||||
if (forceHideDuration && duration != null) {
|
||||
duration.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
this.audioSlide = audio;
|
||||
}
|
||||
|
||||
public void setDownloadClickListener(@Nullable SlideClickListener listener) {
|
||||
|
||||
@@ -9,13 +9,11 @@ import android.text.Annotation;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.Selection;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextUtils.TruncateAt;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
@@ -49,7 +47,6 @@ import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.List;
|
||||
@@ -65,7 +62,6 @@ public class ComposeText extends EmojiEditText {
|
||||
private static final Pattern TIME_PATTERN = Pattern.compile("^[0-9]{1,2}:[0-9]{1,2}$");
|
||||
|
||||
private CharSequence hint;
|
||||
private SpannableString subHint;
|
||||
private MentionRendererDelegate mentionRendererDelegate;
|
||||
private SpoilerRendererDelegate spoilerRendererDelegate;
|
||||
private MentionValidatorWatcher mentionValidatorWatcher;
|
||||
@@ -106,13 +102,7 @@ public class ComposeText extends EmojiEditText {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
if (getLayout() != null && !TextUtils.isEmpty(hint)) {
|
||||
if (!TextUtils.isEmpty(subHint)) {
|
||||
setHintWithChecks(new SpannableStringBuilder().append(ellipsizeToWidth(hint))
|
||||
.append("\n")
|
||||
.append(ellipsizeToWidth(subHint)));
|
||||
} else {
|
||||
setHintWithChecks(ellipsizeToWidth(hint));
|
||||
}
|
||||
setHintWithChecks(ellipsizeToWidth(hint));
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
@@ -173,25 +163,9 @@ public class ComposeText extends EmojiEditText {
|
||||
TruncateAt.END);
|
||||
}
|
||||
|
||||
public void setHint(@NonNull String hint, @Nullable CharSequence subHint) {
|
||||
public void setHint(@NonNull String hint) {
|
||||
this.hint = hint;
|
||||
|
||||
if (subHint != null) {
|
||||
this.subHint = new SpannableString(subHint);
|
||||
this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
} else {
|
||||
this.subHint = null;
|
||||
}
|
||||
|
||||
if (this.subHint != null) {
|
||||
setHintWithChecks(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint))
|
||||
.append("\n")
|
||||
.append(ellipsizeToWidth(this.subHint)));
|
||||
} else {
|
||||
setHintWithChecks(ellipsizeToWidth(this.hint));
|
||||
}
|
||||
|
||||
setHintWithChecks(hint);
|
||||
setHintWithChecks(ellipsizeToWidth(this.hint));
|
||||
}
|
||||
|
||||
public void setDraftText(@Nullable CharSequence draftText) {
|
||||
@@ -249,10 +223,7 @@ public class ComposeText extends EmojiEditText {
|
||||
}
|
||||
|
||||
setImeOptions(imeOptions);
|
||||
setHint(getContext().getString(messageSendType.getComposeHintRes()),
|
||||
messageSendType.getSimName() != null
|
||||
? getContext().getString(R.string.conversation_activity__from_sim_name, messageSendType.getSimName())
|
||||
: null);
|
||||
setHint(getContext().getString(messageSendType.getComposeHintRes()));
|
||||
setInputType(inputType);
|
||||
}
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
}
|
||||
String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, timestamp);
|
||||
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage()) {
|
||||
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) {
|
||||
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date);
|
||||
}
|
||||
dateView.setText(date);
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
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.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil
|
||||
import org.thoughtcrime.securesms.util.SupportEmailUtil
|
||||
|
||||
class DebugLogsPromptDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun show(context: Context, fragmentManager: FragmentManager) {
|
||||
if (NetworkUtil.isConnected(context) && fragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) == null) {
|
||||
DebugLogsPromptDialogFragment().apply {
|
||||
arguments = bundleOf()
|
||||
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
SignalStore.uiHints().lastNotificationLogsPrompt = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val peekHeightPercentage: Float = 0.66f
|
||||
override val themeResId: Int = R.style.Widget_Signal_FixedRoundedCorners_Messages
|
||||
|
||||
private val binding by ViewBinderDelegate(PromptLogsBottomSheetBinding::bind)
|
||||
|
||||
private lateinit var viewModel: PromptLogsViewModel
|
||||
|
||||
private val disposables: LifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.prompt_logs_bottom_sheet, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
|
||||
viewModel = ViewModelProvider(this).get(PromptLogsViewModel::class.java)
|
||||
binding.submit.setOnClickListener {
|
||||
val progressDialog = SignalProgressDialog.show(requireContext())
|
||||
disposables += viewModel.submitLogs().subscribe({ result ->
|
||||
submitLogs(result)
|
||||
progressDialog.dismiss()
|
||||
dismiss()
|
||||
}, { _ ->
|
||||
Toast.makeText(requireContext(), getString(R.string.HelpFragment__could_not_upload_logs), Toast.LENGTH_LONG).show()
|
||||
progressDialog.dismiss()
|
||||
dismiss()
|
||||
})
|
||||
}
|
||||
binding.decline.setOnClickListener {
|
||||
SignalStore.uiHints().markDeclinedShareNotificationLogs()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitLogs(debugLog: String) {
|
||||
CommunicationActions.openEmail(
|
||||
requireContext(),
|
||||
SupportEmailUtil.getSupportEmailAddress(requireContext()),
|
||||
getString(R.string.DebugLogsPromptDialogFragment__signal_android_support_request),
|
||||
getEmailBody(debugLog)
|
||||
)
|
||||
}
|
||||
|
||||
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 = ResourceUtil.getEnglishResources(requireContext()).getString(R.string.DebugLogsPromptDialogFragment__slow_notifications_category)
|
||||
return SupportEmailUtil.generateSupportEmailBody(
|
||||
requireContext(),
|
||||
R.string.DebugLogsPromptDialogFragment__signal_android_support_request,
|
||||
" - $category",
|
||||
"\n\n",
|
||||
suffix.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -48,9 +48,13 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
|
||||
setText(recipient, fromString, read, suffix, true);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix, boolean asThread) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
|
||||
if (recipient.isSelf()) {
|
||||
if (asThread && recipient.isSelf()) {
|
||||
builder.append(getContext().getString(R.string.note_to_self));
|
||||
} else {
|
||||
builder.append(fromString);
|
||||
@@ -60,7 +64,7 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
builder.append(suffix);
|
||||
}
|
||||
|
||||
if (recipient.showVerified()) {
|
||||
if (asThread && recipient.showVerified()) {
|
||||
Drawable official = ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_20);
|
||||
official.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20));
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
|
||||
hideInput(resetKeyboardGuideline = false)
|
||||
}
|
||||
|
||||
fun hideAll(imeTarget: EditText) {
|
||||
ViewUtil.hideKeyboard(context, imeTarget)
|
||||
hideInput(resetKeyboardGuideline = true)
|
||||
}
|
||||
|
||||
fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, showSoftKeyOnHide: Boolean = false) {
|
||||
if (fragmentCreator.id == inputId) {
|
||||
if (showSoftKeyOnHide) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.annotation.DimenRes;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
@@ -78,7 +79,7 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class InputPanel extends LinearLayout
|
||||
public class InputPanel extends ConstraintLayout
|
||||
implements AudioRecordingHandler,
|
||||
KeyboardAwareLinearLayout.OnKeyboardShownListener,
|
||||
EmojiEventListener,
|
||||
@@ -101,10 +102,10 @@ public class InputPanel extends LinearLayout
|
||||
private SendButton sendButton;
|
||||
private View recordingContainer;
|
||||
private View recordLockCancel;
|
||||
private ViewGroup composeContainer;
|
||||
private View composeContainer;
|
||||
private View editMessageCancel;
|
||||
private ImageView editMessageThumbnail;
|
||||
private View editMessageHeader;
|
||||
private View editMessageTitle;
|
||||
|
||||
private MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
@@ -163,7 +164,7 @@ public class InputPanel extends LinearLayout
|
||||
TimeUnit.HOURS.toSeconds(1),
|
||||
() -> microphoneRecorderView.cancelAction(false));
|
||||
this.editMessageCancel = findViewById(R.id.input_panel_exit_edit_mode);
|
||||
this.editMessageHeader = findViewById(R.id.edit_message_compose_header);
|
||||
this.editMessageTitle = findViewById(R.id.edit_message_title);
|
||||
this.editMessageThumbnail = findViewById(R.id.edit_message_thumbnail);
|
||||
|
||||
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction(true));
|
||||
@@ -454,13 +455,15 @@ public class InputPanel extends LinearLayout
|
||||
private void updateEditModeUi() {
|
||||
if (inEditMessageMode()) {
|
||||
ViewUtil.focusAndShowKeyboard(composeText);
|
||||
editMessageHeader.setVisibility(View.VISIBLE);
|
||||
editMessageTitle.setVisibility(View.VISIBLE);
|
||||
editMessageThumbnail.setVisibility(View.VISIBLE);
|
||||
editMessageCancel.setVisibility(View.VISIBLE);
|
||||
if (listener != null) {
|
||||
listener.onEnterEditMode();
|
||||
}
|
||||
} else {
|
||||
editMessageHeader.setVisibility(View.GONE);
|
||||
editMessageTitle.setVisibility(View.GONE);
|
||||
editMessageThumbnail.setVisibility(View.GONE);
|
||||
editMessageCancel.setVisibility(View.GONE);
|
||||
if (listener != null) {
|
||||
listener.onExitEditMode();
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsAnimationCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
@@ -45,6 +46,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(InsetAwareConstraintLayout::class.java)
|
||||
private val keyboardType = WindowInsetsCompat.Type.ime()
|
||||
private val windowTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
|
||||
}
|
||||
@@ -61,6 +63,9 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
private var overridingKeyboard: Boolean = false
|
||||
private var previousKeyboardHeight: Int = 0
|
||||
|
||||
val isKeyboardShowing: Boolean
|
||||
get() = previousKeyboardHeight > 0
|
||||
|
||||
init {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsetsCompat ->
|
||||
applyInsets(windowInsets = windowInsetsCompat.getInsets(windowTypes), keyboardInsets = windowInsetsCompat.getInsets(keyboardType))
|
||||
@@ -141,10 +146,12 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
|
||||
SignalStore.misc().keyboardPortraitHeight
|
||||
}
|
||||
|
||||
return if (height <= 0) {
|
||||
resources.getDimensionPixelSize(R.dimen.default_custom_keyboard_size)
|
||||
} else {
|
||||
val minHeight = resources.getDimensionPixelSize(R.dimen.default_custom_keyboard_size)
|
||||
return if (height > minHeight) {
|
||||
height
|
||||
} else {
|
||||
Log.w(TAG, "Saved keyboard height ($height) is too low, using default size ($minHeight)")
|
||||
minHeight
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.subjects.SingleSubject
|
||||
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
|
||||
|
||||
class PromptLogsViewModel : ViewModel() {
|
||||
|
||||
private val submitDebugLogRepository = SubmitDebugLogRepository()
|
||||
|
||||
fun submitLogs(): Single<String> {
|
||||
val singleSubject = SingleSubject.create<String?>()
|
||||
submitDebugLogRepository.buildAndSubmitLog { result ->
|
||||
if (result.isPresent) {
|
||||
singleSubject.onSuccess(result.get())
|
||||
} else {
|
||||
singleSubject.onError(Throwable())
|
||||
}
|
||||
}
|
||||
|
||||
return singleSubject.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.addTo
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
@@ -27,7 +28,7 @@ class ScrollToPositionDelegate private constructor(
|
||||
private val recyclerView: RecyclerView,
|
||||
canJumpToPosition: (Int) -> Boolean,
|
||||
mapToTruePosition: (Int) -> Int,
|
||||
disposables: CompositeDisposable
|
||||
private val disposables: CompositeDisposable
|
||||
) : Disposable by disposables {
|
||||
companion object {
|
||||
private val TAG = Log.tag(ScrollToPositionDelegate::class.java)
|
||||
@@ -41,10 +42,12 @@ class ScrollToPositionDelegate private constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private val listCommitted = BehaviorSubject.create<Unit>()
|
||||
private val listCommitted = BehaviorSubject.create<Long>()
|
||||
private val scrollPositionRequested = BehaviorSubject.createDefault(EMPTY)
|
||||
private val scrollPositionRequests: Observable<ScrollToPositionRequest> = Observable.combineLatest(listCommitted, scrollPositionRequested) { _, b -> b }
|
||||
|
||||
private var markedListCommittedTimestamp: Long = 0L
|
||||
|
||||
constructor(
|
||||
recyclerView: RecyclerView,
|
||||
canJumpToPosition: (Int) -> Boolean = { true },
|
||||
@@ -91,16 +94,37 @@ class ScrollToPositionDelegate private constructor(
|
||||
requestScrollPosition(0, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the scroll position to 0 after a list update is committed that occurs later
|
||||
* than the version set by [markListCommittedVersion].
|
||||
*/
|
||||
@AnyThread
|
||||
fun resetScrollPositionAfterMarkListVersionSurpassed() {
|
||||
val currentMark = markedListCommittedTimestamp
|
||||
listCommitted
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter { it > currentMark }
|
||||
.firstElement()
|
||||
.subscribeBy {
|
||||
requestScrollPosition(0, true)
|
||||
}
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
/**
|
||||
* This should be called every time a list is submitted to the RecyclerView's adapter.
|
||||
*/
|
||||
@AnyThread
|
||||
fun notifyListCommitted() {
|
||||
listCommitted.onNext(Unit)
|
||||
listCommitted.onNext(System.currentTimeMillis())
|
||||
}
|
||||
|
||||
fun isListCommitted(): Boolean = listCommitted.value != null
|
||||
|
||||
fun markListCommittedVersion() {
|
||||
markedListCommittedTimestamp = listCommitted.value ?: 0L
|
||||
}
|
||||
|
||||
private fun handleScrollPositionRequest(
|
||||
request: ScrollToPositionRequest,
|
||||
recyclerView: RecyclerView
|
||||
|
||||
@@ -6,16 +6,8 @@ import android.view.View
|
||||
import android.view.View.OnLongClickListener
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import java.lang.AssertionError
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* The send button you see in a conversation.
|
||||
@@ -23,122 +15,21 @@ import java.util.concurrent.CopyOnWriteArrayList
|
||||
*/
|
||||
class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImageButton(context, attributeSet), OnLongClickListener {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SendButton::class.java)
|
||||
}
|
||||
|
||||
private val listeners: MutableList<SendTypeChangedListener> = CopyOnWriteArrayList()
|
||||
private var scheduledSendListener: ScheduledSendListener? = null
|
||||
|
||||
private var availableSendTypes: List<MessageSendType> = MessageSendType.getAllAvailable(context, false)
|
||||
private var activeMessageSendType: MessageSendType? = null
|
||||
private var defaultTransportType: MessageSendType.TransportType = MessageSendType.TransportType.SIGNAL
|
||||
private var defaultSubscriptionId: Int? = null
|
||||
|
||||
lateinit var snackbarContainer: View
|
||||
private var popupContainer: ViewGroup? = null
|
||||
|
||||
init {
|
||||
setOnLongClickListener(this)
|
||||
ViewUtil.mirrorIfRtl(this, getContext())
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the [selectedSendType] was chosen manually by the user, otherwise false.
|
||||
*/
|
||||
val isManualSelection: Boolean
|
||||
get() = activeMessageSendType != null
|
||||
|
||||
/**
|
||||
* The actively-selected send type.
|
||||
*/
|
||||
val selectedSendType: MessageSendType
|
||||
get() {
|
||||
activeMessageSendType?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
if (defaultTransportType === MessageSendType.TransportType.SMS) {
|
||||
for (type in availableSendTypes) {
|
||||
if (type.usesSmsTransport && (defaultSubscriptionId == null || type.simSubscriptionId == defaultSubscriptionId)) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (type in availableSendTypes) {
|
||||
if (type.transportType === defaultTransportType) {
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
Log.w(TAG, "No options of default type! Resetting. DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
|
||||
|
||||
val signalType: MessageSendType? = availableSendTypes.firstOrNull { it.usesSignalTransport }
|
||||
if (signalType != null) {
|
||||
Log.w(TAG, "No options of default type, but Signal type is available. Switching. DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
|
||||
defaultTransportType = MessageSendType.TransportType.SIGNAL
|
||||
onSelectionChanged(signalType, false)
|
||||
return signalType
|
||||
} else if (availableSendTypes.isEmpty()) {
|
||||
Log.w(TAG, "No send types available at all! Enabling the Signal transport.")
|
||||
defaultTransportType = MessageSendType.TransportType.SIGNAL
|
||||
availableSendTypes = listOf(MessageSendType.SignalMessageSendType)
|
||||
onSelectionChanged(MessageSendType.SignalMessageSendType, false)
|
||||
return MessageSendType.SignalMessageSendType
|
||||
} else {
|
||||
throw AssertionError("No options of default type! DefaultTransportType: $defaultTransportType, AllAvailable: ${availableSendTypes.map { it.transportType }}")
|
||||
}
|
||||
}
|
||||
|
||||
fun addOnSelectionChangedListener(listener: SendTypeChangedListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun triggerSelectedChangedEvent() {
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
setImageResource(MessageSendType.SignalMessageSendType.buttonDrawableRes)
|
||||
contentDescription = context.getString(MessageSendType.SignalMessageSendType.titleRes)
|
||||
}
|
||||
|
||||
fun setScheduledSendListener(listener: ScheduledSendListener?) {
|
||||
this.scheduledSendListener = listener
|
||||
}
|
||||
|
||||
fun resetAvailableTransports(isMediaMessage: Boolean) {
|
||||
availableSendTypes = MessageSendType.getAllAvailable(context, isMediaMessage)
|
||||
activeMessageSendType = null
|
||||
defaultTransportType = MessageSendType.TransportType.SIGNAL
|
||||
defaultSubscriptionId = null
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
fun disableTransportType(type: MessageSendType.TransportType) {
|
||||
availableSendTypes = availableSendTypes.filterNot { it.transportType == type }
|
||||
}
|
||||
|
||||
fun setDefaultTransport(type: MessageSendType.TransportType) {
|
||||
if (defaultTransportType == type) {
|
||||
return
|
||||
}
|
||||
defaultTransportType = type
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
fun setSendType(sendType: MessageSendType?) {
|
||||
if (activeMessageSendType == sendType) {
|
||||
return
|
||||
}
|
||||
activeMessageSendType = sendType
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = true)
|
||||
}
|
||||
|
||||
fun setDefaultSubscriptionId(subscriptionId: Int?) {
|
||||
if (defaultSubscriptionId == subscriptionId) {
|
||||
return
|
||||
}
|
||||
defaultSubscriptionId = subscriptionId
|
||||
onSelectionChanged(newType = selectedSendType, isManualSelection = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called with a view that is acceptable for determining the bounds of the popup selector.
|
||||
*/
|
||||
@@ -146,78 +37,19 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
|
||||
popupContainer = container
|
||||
}
|
||||
|
||||
private fun onSelectionChanged(newType: MessageSendType, isManualSelection: Boolean) {
|
||||
setImageResource(newType.buttonDrawableRes)
|
||||
contentDescription = context.getString(newType.titleRes)
|
||||
|
||||
for (listener in listeners) {
|
||||
listener.onSendTypeChanged(newType, isManualSelection)
|
||||
}
|
||||
}
|
||||
|
||||
fun showSendTypeMenu(): Boolean {
|
||||
return if (availableSendTypes.size == 1) {
|
||||
if (scheduledSendListener == null && !SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
|
||||
Snackbar.make(snackbarContainer, R.string.InputPanel__sms_messaging_is_no_longer_supported_in_signal, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
false
|
||||
} else {
|
||||
showSendTypeContextMenu(false)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongClick(v: View): Boolean {
|
||||
if (!isEnabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
val scheduleListener = scheduledSendListener
|
||||
if (availableSendTypes.size == 1) {
|
||||
return if (scheduleListener?.canSchedule() == true && selectedSendType.transportType != MessageSendType.TransportType.SMS) {
|
||||
scheduleListener.onSendScheduled()
|
||||
true
|
||||
} else if (!SignalStore.misc().smsExportPhase.allowSmsFeatures()) {
|
||||
Snackbar.make(snackbarContainer, R.string.InputPanel__sms_messaging_is_no_longer_supported_in_signal, Snackbar.LENGTH_SHORT).show()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
return if (scheduleListener?.canSchedule() == true) {
|
||||
scheduleListener.onSendScheduled()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
showSendTypeContextMenu(selectedSendType.transportType != MessageSendType.TransportType.SMS)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showSendTypeContextMenu(allowScheduling: Boolean) {
|
||||
val currentlySelected: MessageSendType = selectedSendType
|
||||
val listener = scheduledSendListener
|
||||
val items = availableSendTypes
|
||||
.filterNot { it == currentlySelected }
|
||||
.map { option ->
|
||||
ActionItem(
|
||||
iconRes = option.menuDrawableRes,
|
||||
title = option.getTitle(context),
|
||||
action = { setSendType(option) }
|
||||
)
|
||||
}.toMutableList()
|
||||
if (allowScheduling && listener?.canSchedule() == true) {
|
||||
items += ActionItem(
|
||||
iconRes = R.drawable.symbol_calendar_24,
|
||||
title = context.getString(R.string.conversation_activity__option_schedule_message),
|
||||
action = { listener.onSendScheduled() }
|
||||
)
|
||||
}
|
||||
|
||||
SignalContextMenu.Builder((parent as View), popupContainer!!)
|
||||
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.ABOVE)
|
||||
.offsetY(ViewUtil.dpToPx(8))
|
||||
.show(items)
|
||||
}
|
||||
|
||||
fun interface SendTypeChangedListener {
|
||||
fun onSendTypeChanged(newType: MessageSendType, manuallySelected: Boolean)
|
||||
}
|
||||
|
||||
interface ScheduledSendListener {
|
||||
|
||||
@@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
@@ -378,7 +379,7 @@ public class ThumbnailView extends FrameLayout {
|
||||
this.playOverlay.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (Util.equals(slide, this.slide)) {
|
||||
if (hasSameContents(this.slide, slide)) {
|
||||
Log.i(TAG, "Not re-loading slide " + slide.asAttachment().getUri());
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
@@ -609,6 +610,20 @@ public class ThumbnailView extends FrameLayout {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static boolean hasSameContents(@Nullable Slide slide, @Nullable Slide other) {
|
||||
if (Util.equals(slide, other)) {
|
||||
|
||||
if (slide != null && other != null) {
|
||||
byte[] digestLeft = slide.asAttachment().getDigest();
|
||||
byte[] digestRight = other.asAttachment().getDigest();
|
||||
|
||||
return Arrays.equals(digestLeft, digestRight);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface ThumbnailRequestListener extends RequestListener<Drawable> {
|
||||
void onLoadCanceled();
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ public class TooltipPopup extends PopupWindow {
|
||||
private void show() {
|
||||
if (anchor.getWidth() == 0 && anchor.getHeight() == 0) {
|
||||
anchor.post(this::show);
|
||||
return;
|
||||
}
|
||||
|
||||
getContentView().measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||
|
||||
@@ -90,6 +90,12 @@ public class EmojiEditText extends AppCompatEditText {
|
||||
onFocusChangeListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeOnFocusChangeListener(@Nullable OnFocusChangeListener listener) {
|
||||
if (listener != null) {
|
||||
onFocusChangeListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters, boolean jumboEmoji) {
|
||||
InputFilter[] result;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.emoji;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
@@ -18,6 +19,8 @@ import android.text.method.TransformationMethod;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
@@ -25,6 +28,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.GestureDetectorCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
@@ -34,6 +38,7 @@ import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||
import org.thoughtcrime.securesms.components.spoiler.SpoilerRendererDelegate;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@@ -151,7 +156,12 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
public void setText(@Nullable CharSequence text, BufferType type) {
|
||||
EmojiParser.CandidateList candidates = isInEditMode() ? null : EmojiProvider.getCandidates(text);
|
||||
|
||||
if (scaleEmojis && candidates != null && candidates.allEmojis && (candidates.hasJumboForAll() || JumboEmoji.canDownloadJumbo(getContext()))) {
|
||||
if (scaleEmojis &&
|
||||
candidates != null &&
|
||||
candidates.allEmojis &&
|
||||
(candidates.hasJumboForAll() || JumboEmoji.canDownloadJumbo(getContext())) &&
|
||||
(text instanceof Spanned && !MessageStyler.hasStyling((Spanned) text)))
|
||||
{
|
||||
int emojis = candidates.size();
|
||||
float scale = 1.0f;
|
||||
|
||||
@@ -309,6 +319,12 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void bindGestureListener() {
|
||||
GestureDetectorCompat gestureDetectorCompat = new GestureDetectorCompat(getContext(), new OnGestureListener());
|
||||
setOnTouchListener((v, event) -> gestureDetectorCompat.onTouchEvent(event));
|
||||
}
|
||||
|
||||
private void ellipsizeAnyTextForMaxLength() {
|
||||
if (maxLength > 0 && getText().length() > maxLength + 1) {
|
||||
SpannableStringBuilder newContent = new SpannableStringBuilder();
|
||||
@@ -459,4 +475,32 @@ public class EmojiTextView extends AppCompatTextView {
|
||||
mentionRendererDelegate.setTint(mentionBackgroundTint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Due to some peculiarities in how TextView deals with touch events, it's really easy to accidentally trigger
|
||||
* a click (say, when you try to scroll but you're at the bottom of a view.) Because of this, we handle these
|
||||
* events manually.
|
||||
*/
|
||||
private class OnGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
@Override
|
||||
public boolean onDown(@NonNull MotionEvent e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
|
||||
if (!canScrollVertically((int) distanceY)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int maxScrollDistance = computeVerticalScrollRange() - computeHorizontalScrollExtent();
|
||||
scrollTo(0, Util.clamp(getScrollY() + (int) distanceY, 0, maxScrollDistance));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
|
||||
return performClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ public class SignalMapView extends LinearLayout {
|
||||
public static ListenableFuture<Bitmap> snapshot(final LatLng place, @NonNull final MapView mapView) {
|
||||
final SettableFuture<Bitmap> future = new SettableFuture<>();
|
||||
mapView.onCreate(null);
|
||||
mapView.onStart();
|
||||
mapView.onResume();
|
||||
|
||||
mapView.setVisibility(View.VISIBLE);
|
||||
@@ -91,6 +92,7 @@ public class SignalMapView extends LinearLayout {
|
||||
future.set(bitmap);
|
||||
mapView.setVisibility(View.GONE);
|
||||
mapView.onPause();
|
||||
mapView.onStop();
|
||||
mapView.onDestroy();
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.PowerManagerCompat;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
@@ -25,9 +26,7 @@ public class DozeReminder extends Reminder {
|
||||
|
||||
setOkListener(v -> {
|
||||
TextSecurePreferences.setPromptedOptimizeDoze(context, true);
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||
Uri.parse("package:" + context.getPackageName()));
|
||||
context.startActivity(intent);
|
||||
PowerManagerCompat.requestIgnoreBatteryOptimizations(context);
|
||||
});
|
||||
|
||||
setDismissListener(v -> TextSecurePreferences.setPromptedOptimizeDoze(context, true));
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public final class FirstInviteReminder extends Reminder {
|
||||
|
||||
private final int percentIncrease;
|
||||
|
||||
public FirstInviteReminder(final int percentIncrease) {
|
||||
super(R.string.FirstInviteReminder__title, NO_RESOURCE);
|
||||
this.percentIncrease = percentIncrease;
|
||||
|
||||
addAction(new Action(R.string.InsightsReminder__invite, R.id.reminder_action_invite));
|
||||
addAction(new Action(R.string.InsightsReminder__view_insights, R.id.reminder_action_view_insights));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull CharSequence getText(@NonNull Context context) {
|
||||
return context.getString(R.string.FirstInviteReminder__description, percentIncrease);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
public final class SecondInviteReminder extends Reminder {
|
||||
|
||||
private final Recipient recipient;
|
||||
private final int progress;
|
||||
|
||||
public SecondInviteReminder(final @NonNull Context context,
|
||||
final @NonNull Recipient recipient,
|
||||
final int percent)
|
||||
{
|
||||
super(R.string.SecondInviteReminder__title, NO_RESOURCE);
|
||||
this.recipient = recipient;
|
||||
|
||||
this.progress = percent;
|
||||
|
||||
addAction(new Action(R.string.InsightsReminder__invite, R.id.reminder_action_invite));
|
||||
addAction(new Action(R.string.InsightsReminder__view_insights, R.id.reminder_action_view_insights));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull CharSequence getText(@NonNull Context context) {
|
||||
return context.getString(R.string.SecondInviteReminder__description, recipient.getDisplayName(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class UnauthorizedReminder extends Reminder {
|
||||
|
||||
public UnauthorizedReminder(final Context context) {
|
||||
public UnauthorizedReminder() {
|
||||
super(R.string.UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device);
|
||||
|
||||
setOkListener(v -> {
|
||||
context.startActivity(RegistrationNavigationActivity.newIntentForReRegistration(context));
|
||||
});
|
||||
|
||||
addAction(new Action(R.string.UnauthorizedReminder_reregister_action, R.id.reminder_action_re_register));
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ abstract class DSLSettingsFragment(
|
||||
return null
|
||||
}
|
||||
|
||||
return Material3OnScrollHelper(requireActivity(), toolbar)
|
||||
return Material3OnScrollHelper(requireActivity(), toolbar, viewLifecycleOwner)
|
||||
}
|
||||
|
||||
open fun onToolbarNavigationClicked() {
|
||||
|
||||
@@ -75,7 +75,7 @@ class AppSettingsFragment : DSLSettingsFragment(
|
||||
if (ExpiredBuildReminder.isEligible()) {
|
||||
showReminder(ExpiredBuildReminder(context))
|
||||
} else if (UnauthorizedReminder.isEligible(context)) {
|
||||
showReminder(UnauthorizedReminder(context))
|
||||
showReminder(UnauthorizedReminder())
|
||||
} else {
|
||||
hideReminders()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.getCaptchaArguments
|
||||
@@ -13,6 +14,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.registration.fragments.BaseEnterSmsCodeFragment
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberEnterSmsCodeFragment::class.java)
|
||||
|
||||
class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment<ChangeNumberViewModel>(R.layout.fragment_change_number_enter_code) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@@ -20,7 +23,10 @@ class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment<ChangeNumberVi
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbar.title = viewModel.number.fullFormattedNumber
|
||||
toolbar.setNavigationOnClickListener { navigateUp() }
|
||||
toolbar.setNavigationOnClickListener {
|
||||
Log.d(TAG, "Toolbar navigation clicked.")
|
||||
navigateUp()
|
||||
}
|
||||
|
||||
view.findViewById<View>(R.id.verify_header).setOnClickListener(null)
|
||||
|
||||
@@ -28,6 +34,7 @@ class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment<ChangeNumberVi
|
||||
viewLifecycleOwner,
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
Log.d(TAG, "onBackPressed")
|
||||
navigateUp()
|
||||
}
|
||||
}
|
||||
@@ -36,8 +43,10 @@ class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment<ChangeNumberVi
|
||||
|
||||
private fun navigateUp() {
|
||||
if (SignalStore.misc().isChangeNumberLocked) {
|
||||
Log.d(TAG, "Change number locked, navigateUp")
|
||||
startActivity(ChangeNumberLockActivity.createIntent(requireContext()))
|
||||
} else {
|
||||
Log.d(TAG, "navigateUp")
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
@@ -47,18 +56,22 @@ class ChangeNumberEnterSmsCodeFragment : BaseEnterSmsCodeFragment<ChangeNumberVi
|
||||
}
|
||||
|
||||
override fun handleSuccessfulVerify() {
|
||||
Log.d(TAG, "handleSuccessfulVerify")
|
||||
displaySuccess { changeNumberSuccess() }
|
||||
}
|
||||
|
||||
override fun navigateToCaptcha() {
|
||||
Log.d(TAG, "navigateToCaptcha")
|
||||
findNavController().safeNavigate(R.id.action_changeNumberEnterCodeFragment_to_captchaFragment, getCaptchaArguments())
|
||||
}
|
||||
|
||||
override fun navigateToRegistrationLock(timeRemaining: Long) {
|
||||
Log.d(TAG, "navigateToRegistrationLock")
|
||||
findNavController().safeNavigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberRegistrationLock(timeRemaining))
|
||||
}
|
||||
|
||||
override fun navigateToKbsAccountLocked() {
|
||||
Log.d(TAG, "navigateToKbsAccountLocked")
|
||||
findNavController().safeNavigate(ChangeNumberEnterSmsCodeFragmentDirections.actionChangeNumberEnterCodeFragmentToChangeNumberAccountLocked())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.Objects
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
|
||||
|
||||
@@ -34,8 +34,7 @@ import org.whispersystems.signalservice.api.SvrNoDataException
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.account.PreKeyUpload
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
@@ -243,7 +242,7 @@ class ChangeNumberRepository(
|
||||
throw AssertionError("No change number metadata")
|
||||
}
|
||||
|
||||
val originalPni = ServiceId.fromByteString(metadata.previousPni)
|
||||
val originalPni = PNI.parseOrThrow(metadata.previousPni)
|
||||
|
||||
if (originalPni == pni) {
|
||||
Log.i(TAG, "No change has occurred, PNI is unchanged: $pni")
|
||||
|
||||
@@ -81,7 +81,7 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon
|
||||
|
||||
val processor: RegistrationSessionProcessor = (result as RequestCodeResult.RequestedVerificationCode).processor
|
||||
|
||||
if (processor.hasResult()) {
|
||||
if (processor.verificationCodeRequestSuccess()) {
|
||||
Log.i(TAG, "Successfully requested SMS code.")
|
||||
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
|
||||
} else if (processor.captchaRequired(viewModel.excludedChallenges)) {
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewMod
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.push.exceptions.IncorrectCodeException
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import java.util.Objects
|
||||
|
||||
@@ -616,14 +616,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from("Use V2 ConversationFragment"),
|
||||
isChecked = state.useConversationFragmentV2,
|
||||
onClick = {
|
||||
viewModel.setUseConversationFragmentV2(!state.useConversationFragmentV2)
|
||||
}
|
||||
)
|
||||
|
||||
switchPref(
|
||||
title = DSLSettingsText.from("Use V2 ConversationItem"),
|
||||
isChecked = state.useConversationItemV2,
|
||||
|
||||
@@ -22,6 +22,5 @@ data class InternalSettingsState(
|
||||
val disableStorageService: Boolean,
|
||||
val canClearOnboardingState: Boolean,
|
||||
val pnpInitialized: Boolean,
|
||||
val useConversationFragmentV2: Boolean,
|
||||
val useConversationItemV2: Boolean
|
||||
)
|
||||
|
||||
@@ -104,11 +104,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setUseConversationFragmentV2(enabled: Boolean) {
|
||||
SignalStore.internalValues().setUseConversationFragmentV2(enabled)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun setUseConversationItemV2(enabled: Boolean) {
|
||||
SignalStore.internalValues().setUseConversationItemV2(enabled)
|
||||
refresh()
|
||||
@@ -141,7 +136,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
|
||||
disableStorageService = SignalStore.internalValues().storageServiceDisabled(),
|
||||
canClearOnboardingState = SignalStore.storyValues().hasDownloadedOnboardingStory && Stories.isFeatureEnabled(),
|
||||
pnpInitialized = SignalStore.misc().hasPniInitializedDevices(),
|
||||
useConversationFragmentV2 = SignalStore.internalValues().useConversationFragmentV2(),
|
||||
useConversationItemV2 = SignalStore.internalValues().useConversationItemV2()
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ package org.thoughtcrime.securesms.components.settings.app.internal.search
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
@@ -46,7 +45,7 @@ class InternalSearchViewModel : ViewModel() {
|
||||
InternalSearchResult(
|
||||
id = record.id,
|
||||
name = record.displayName(),
|
||||
aci = record.serviceId?.toString(),
|
||||
aci = record.aci?.toString(),
|
||||
pni = record.pni.toString(),
|
||||
groupId = record.groupId
|
||||
)
|
||||
@@ -70,10 +69,10 @@ class InternalSearchViewModel : ViewModel() {
|
||||
|
||||
private fun RecipientRecord.displayName(): String {
|
||||
return when {
|
||||
this.groupType == RecipientTable.GroupType.SIGNAL_V1 -> "GV1::${this.groupId}"
|
||||
this.groupType == RecipientTable.GroupType.SIGNAL_V2 -> "GV2::${this.groupId}"
|
||||
this.groupType == RecipientTable.GroupType.MMS -> "MMS_GROUP::${this.groupId}"
|
||||
this.groupType == RecipientTable.GroupType.DISTRIBUTION_LIST -> "DLIST::${this.distributionListId}"
|
||||
this.recipientType == RecipientTable.RecipientType.GV1 -> "GV1::${this.groupId}"
|
||||
this.recipientType == RecipientTable.RecipientType.GV2 -> "GV2::${this.groupId}"
|
||||
this.recipientType == RecipientTable.RecipientType.MMS -> "MMS_GROUP::${this.groupId}"
|
||||
this.recipientType == RecipientTable.RecipientType.DISTRIBUTION_LIST -> "DLIST::${this.distributionListId}"
|
||||
this.systemDisplayName?.isNotBlank() == true -> this.systemDisplayName
|
||||
this.signalProfileName.toString().isNotBlank() -> this.signalProfileName.serialize()
|
||||
this.e164 != null -> this.e164
|
||||
|
||||
@@ -413,6 +413,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
Log.w(TAG, "Unable to authenticate payment lock")
|
||||
viewModel.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences
|
||||
|
||||
builder
|
||||
.setCustomTitle(title)
|
||||
.setOnDismissListener { viewModel.refresh() }
|
||||
.show()
|
||||
} else {
|
||||
startActivity(RegistrationNavigationActivity.newIntentForReRegistration(requireContext()))
|
||||
|
||||
@@ -99,7 +99,7 @@ class DonateToSignalFragment :
|
||||
}
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
|
||||
return object : Material3OnScrollHelper(requireActivity(), toolbar!!) {
|
||||
return object : Material3OnScrollHelper(requireActivity(), toolbar!!, viewLifecycleOwner) {
|
||||
override val activeColorSet: ColorSet = ColorSet(R.color.transparent, R.color.signal_colorBackground)
|
||||
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent, R.color.signal_colorBackground)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadin
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
@@ -84,7 +83,7 @@ class ManageDonationsFragment :
|
||||
}
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
|
||||
return object : Material3OnScrollHelper(requireActivity(), toolbar!!) {
|
||||
return object : Material3OnScrollHelper(requireActivity(), toolbar!!, viewLifecycleOwner) {
|
||||
override val activeColorSet: ColorSet = ColorSet(R.color.transparent, R.color.signal_colorBackground)
|
||||
override val inactiveColorSet: ColorSet = ColorSet(R.color.transparent, R.color.signal_colorBackground)
|
||||
}
|
||||
@@ -245,15 +244,13 @@ class ManageDonationsFragment :
|
||||
|
||||
sectionHeaderPref(R.string.ManageDonationsFragment__other_ways_to_give)
|
||||
|
||||
if (Recipient.self().giftBadgesCapability == Recipient.Capability.SUPPORTED) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__donate_for_a_friend),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_gift_24),
|
||||
onClick = {
|
||||
startActivity(Intent(requireContext(), GiftFlowActivity::class.java))
|
||||
}
|
||||
)
|
||||
}
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__donate_for_a_friend),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_gift_24),
|
||||
onClick = {
|
||||
startActivity(Intent(requireContext(), GiftFlowActivity::class.java))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentBadges() {
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
|
||||
return object : Material3OnScrollHelper(requireActivity(), toolbar!!) {
|
||||
return object : Material3OnScrollHelper(requireActivity(), toolbar!!, viewLifecycleOwner) {
|
||||
override val inactiveColorSet = ColorSet(
|
||||
toolbarColorRes = R.color.signal_colorBackground_0,
|
||||
statusBarColorRes = R.color.signal_colorBackground
|
||||
@@ -543,16 +543,14 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientState.identityRecord != null) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__view_safety_number),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_safety_number_24),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
startActivity(VerifyIdentityActivity.newIntent(requireActivity(), recipientState.identityRecord))
|
||||
}
|
||||
)
|
||||
}
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__view_safety_number),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_safety_number_24),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
VerifyIdentityActivity.startOrShowExchangeMessagesDialog(requireActivity(), recipientState.identityRecord)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (state.sharedMedia != null && state.sharedMedia.count > 0) {
|
||||
|
||||
@@ -153,8 +153,8 @@ class ConversationSettingsRepository(
|
||||
if (groupRecord.isV2Group) {
|
||||
val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
|
||||
val pendingMembers: List<RecipientId> = decryptedGroup.pendingMembersList
|
||||
.map(DecryptedPendingMember::getUuid)
|
||||
.map(GroupProtoUtil::uuidByteStringToRecipientId)
|
||||
.map(DecryptedPendingMember::getServiceIdBytes)
|
||||
.map(GroupProtoUtil::serviceIdBinaryToRecipientId)
|
||||
|
||||
val members = mutableListOf<RecipientId>()
|
||||
|
||||
|
||||
@@ -165,8 +165,11 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (recipient.hasServiceId()) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireServiceId().toString())
|
||||
if (recipient.hasAci()) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireAci().toString())
|
||||
}
|
||||
if (recipient.hasPni()) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requirePni().toString())
|
||||
}
|
||||
}
|
||||
.show()
|
||||
@@ -182,14 +185,25 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
SignalDatabase.threads.deleteConversation(SignalDatabase.threads.getThreadIdIfExistsFor(recipient.id))
|
||||
|
||||
if (recipient.hasServiceId()) {
|
||||
SignalDatabase.recipients.debugClearServiceIds(recipient.id)
|
||||
SignalDatabase.recipients.debugClearProfileData(recipient.id)
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireServiceId().toString())
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requireServiceId().toString())
|
||||
ApplicationDependencies.getProtocolStore().pni().identities().delete(recipient.requireServiceId().toString())
|
||||
SignalDatabase.threads.deleteConversation(SignalDatabase.threads.getThreadIdIfExistsFor(recipient.id))
|
||||
}
|
||||
|
||||
if (recipient.hasAci()) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireAci().toString())
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requirePni(), addressName = recipient.requireAci().toString())
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requireAci().toString())
|
||||
}
|
||||
|
||||
if (recipient.hasPni()) {
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requirePni().toString())
|
||||
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requirePni(), addressName = recipient.requirePni().toString())
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requirePni().toString())
|
||||
}
|
||||
|
||||
startActivity(MainActivity.clearTop(requireContext()))
|
||||
}
|
||||
.show()
|
||||
@@ -237,7 +251,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
SignalDatabase.recipients.debugClearE164AndPni(recipient.id)
|
||||
|
||||
val splitRecipientId: RecipientId = if (FeatureFlags.phoneNumberPrivacy()) {
|
||||
SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.pni.orElse(null), recipient.pni.orElse(null), recipient.requireE164())
|
||||
SignalDatabase.recipients.getAndPossiblyMergePnpVerified(null, recipient.pni.orElse(null), recipient.requireE164())
|
||||
} else {
|
||||
SignalDatabase.recipients.getAndPossiblyMerge(recipient.pni.orElse(null), recipient.requireE164())
|
||||
}
|
||||
@@ -281,7 +295,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
|
||||
SignalDatabase.recipients.debugRemoveAci(recipient.id)
|
||||
|
||||
val aciRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.requireServiceId(), null, null)
|
||||
val aciRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.requireAci(), null, null)
|
||||
|
||||
recipient.profileKey?.let { profileKey ->
|
||||
SignalDatabase.recipients.setProfileKey(aciRecipientId, ProfileKey(profileKey))
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.verify
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Outline
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.view.animation.ScaleAnimation
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextSwitcher
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.libsignal.protocol.fingerprint.Fingerprint
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.qr.QrCodeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.nio.charset.Charset
|
||||
import java.util.Locale
|
||||
|
||||
class SafetyNumberQrView : ConstraintLayout {
|
||||
|
||||
companion object {
|
||||
private const val NUMBER_OF_SEGMENTS = 12
|
||||
|
||||
@JvmStatic
|
||||
fun getSegments(fingerprint: Fingerprint): Array<String> {
|
||||
val segments = arrayOfNulls<String>(NUMBER_OF_SEGMENTS)
|
||||
val digits = fingerprint.displayableFingerprint.displayText
|
||||
val partSize = digits.length / NUMBER_OF_SEGMENTS
|
||||
for (i in 0 until NUMBER_OF_SEGMENTS) {
|
||||
segments[i] = digits.substring(i * partSize, i * partSize + partSize)
|
||||
}
|
||||
return (0 until NUMBER_OF_SEGMENTS).map { digits.substring(it * partSize, it * partSize + partSize) }.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defaultStyle: Int) : super(context, attrs, defaultStyle)
|
||||
|
||||
private val codes: Array<TextView>
|
||||
|
||||
val numbersContainer: View
|
||||
val qrCodeContainer: View
|
||||
|
||||
val shareButton: ImageView
|
||||
|
||||
private val loading: View
|
||||
private val qrCode: ImageView
|
||||
private val qrVerified: ImageView
|
||||
private val tapLabel: TextSwitcher
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.safety_number_qr_view, this)
|
||||
|
||||
numbersContainer = findViewById(R.id.number_table)
|
||||
loading = findViewById(R.id.loading)
|
||||
qrCodeContainer = findViewById(R.id.qr_code_container)
|
||||
qrCode = findViewById(R.id.qr_code)
|
||||
qrVerified = findViewById(R.id.qr_verified)
|
||||
tapLabel = findViewById(R.id.tap_label)
|
||||
|
||||
codes = arrayOf(
|
||||
findViewById(R.id.code_first),
|
||||
findViewById(R.id.code_second),
|
||||
findViewById(R.id.code_third),
|
||||
findViewById(R.id.code_fourth),
|
||||
findViewById(R.id.code_fifth),
|
||||
findViewById(R.id.code_sixth),
|
||||
findViewById(R.id.code_seventh),
|
||||
findViewById(R.id.code_eighth),
|
||||
findViewById(R.id.code_ninth),
|
||||
findViewById(R.id.code_tenth),
|
||||
findViewById(R.id.code_eleventh),
|
||||
findViewById(R.id.code_twelth)
|
||||
)
|
||||
|
||||
shareButton = findViewById(R.id.share)
|
||||
|
||||
outlineProvider = object : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setRoundRect(0, 0, view.width, view.height, 24.dp.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
clipToOutline = true
|
||||
setSafetyNumberType(false)
|
||||
}
|
||||
|
||||
fun setFingerprintViews(fingerprint: Fingerprint, animate: Boolean) {
|
||||
val segments: Array<String> = getSegments(fingerprint)
|
||||
for (i in codes.indices) {
|
||||
if (animate) setCodeSegment(codes[i], segments[i]) else codes[i].text = segments[i]
|
||||
}
|
||||
|
||||
val qrCodeData = fingerprint.scannableFingerprint.serialized
|
||||
val qrCodeString = String(qrCodeData, Charset.forName("ISO-8859-1"))
|
||||
val qrCodeBitmap = QrCodeUtil.createNoPadding(qrCodeString)
|
||||
|
||||
qrCode.setImageBitmap(qrCodeBitmap)
|
||||
shareButton.visible = true
|
||||
|
||||
if (animate) {
|
||||
ViewUtil.fadeIn(qrCode, 1000)
|
||||
ViewUtil.fadeIn(tapLabel, 1000)
|
||||
ViewUtil.fadeOut(loading, 300, GONE)
|
||||
} else {
|
||||
qrCode.visibility = VISIBLE
|
||||
tapLabel.visibility = VISIBLE
|
||||
loading.visibility = GONE
|
||||
}
|
||||
}
|
||||
|
||||
fun setSafetyNumberType(newType: Boolean) {
|
||||
if (newType) {
|
||||
ImageViewCompat.setImageTintList(shareButton, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.signal_dark_colorOnSurface)))
|
||||
setBackgroundColor(ContextCompat.getColor(context, R.color.safety_number_card_blue))
|
||||
codes.forEach {
|
||||
it.setTextColor(ContextCompat.getColor(context, R.color.signal_light_colorOnPrimary))
|
||||
}
|
||||
} else {
|
||||
ImageViewCompat.setImageTintList(shareButton, ColorStateList.valueOf(ContextCompat.getColor(context, R.color.signal_light_colorOnSurface)))
|
||||
setBackgroundColor(ContextCompat.getColor(context, R.color.safety_number_card_grey))
|
||||
codes.forEach {
|
||||
it.setTextColor(ContextCompat.getColor(context, R.color.signal_light_colorOnSurfaceVariant))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun animateVerifiedSuccess() {
|
||||
val qrBitmap = (qrCode.drawable as BitmapDrawable).bitmap
|
||||
val qrSuccess: Bitmap = createVerifiedBitmap(qrBitmap.width, qrBitmap.height, R.drawable.ic_check_white_48dp)
|
||||
qrVerified.setImageBitmap(qrSuccess)
|
||||
qrVerified.background.setColorFilter(resources.getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY)
|
||||
tapLabel.setText(context.getString(R.string.verify_display_fragment__successful_match))
|
||||
animateVerified()
|
||||
}
|
||||
|
||||
fun animateVerifiedFailure() {
|
||||
val qrBitmap = (qrCode.drawable as BitmapDrawable).bitmap
|
||||
val qrSuccess: Bitmap = createVerifiedBitmap(qrBitmap.width, qrBitmap.height, R.drawable.ic_close_white_48dp)
|
||||
qrVerified.setImageBitmap(qrSuccess)
|
||||
qrVerified.background.setColorFilter(resources.getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY)
|
||||
tapLabel.setText(context.getString(R.string.verify_display_fragment__failed_to_verify_safety_number))
|
||||
animateVerified()
|
||||
}
|
||||
|
||||
private fun animateVerified() {
|
||||
val scaleAnimation = ScaleAnimation(
|
||||
0f,
|
||||
1f,
|
||||
0f,
|
||||
1f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF,
|
||||
0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF,
|
||||
0.5f
|
||||
)
|
||||
scaleAnimation.interpolator = FastOutSlowInInterpolator()
|
||||
scaleAnimation.duration = 800
|
||||
scaleAnimation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {}
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
qrVerified.postDelayed({
|
||||
val scaleAnimation = ScaleAnimation(
|
||||
1f,
|
||||
0f,
|
||||
1f,
|
||||
0f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF,
|
||||
0.5f,
|
||||
ScaleAnimation.RELATIVE_TO_SELF,
|
||||
0.5f
|
||||
)
|
||||
scaleAnimation.interpolator = AnticipateInterpolator()
|
||||
scaleAnimation.duration = 500
|
||||
ViewUtil.animateOut(qrVerified, scaleAnimation, GONE)
|
||||
ViewUtil.fadeIn(qrCode, 800)
|
||||
qrCodeContainer.isEnabled = true
|
||||
tapLabel.setText(context.getString(R.string.verify_display_fragment__tap_to_scan))
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) {}
|
||||
})
|
||||
ViewUtil.fadeOut(qrCode, 200, INVISIBLE)
|
||||
ViewUtil.animateIn(qrVerified, scaleAnimation)
|
||||
qrCodeContainer.isEnabled = false
|
||||
}
|
||||
|
||||
private fun createVerifiedBitmap(width: Int, height: Int, @DrawableRes id: Int): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
val check = BitmapFactory.decodeResource(resources, id)
|
||||
val offset = ((width - check.width) / 2).toFloat()
|
||||
canvas.drawBitmap(check, offset, offset, null)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun setCodeSegment(codeView: TextView, segment: String) {
|
||||
val valueAnimator = ValueAnimator.ofInt(0, segment.toInt())
|
||||
valueAnimator.addUpdateListener { animation: ValueAnimator ->
|
||||
val value = animation.animatedValue as Int
|
||||
codeView.text = String.format(Locale.getDefault(), "%05d", value)
|
||||
}
|
||||
valueAnimator.duration = 1000
|
||||
valueAnimator.start()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user