mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-15 22:43:19 +01:00
Compare commits
125 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9846517075 | ||
|
|
0f1cc03dc0 | ||
|
|
0e5031ab45 | ||
|
|
0e4926b5ec | ||
|
|
a25e7c6d3e | ||
|
|
4081ac2a83 | ||
|
|
98a528f595 | ||
|
|
680325b5ee | ||
|
|
16668574a9 | ||
|
|
0d8f6de4c1 | ||
|
|
4c0a98d526 | ||
|
|
10f78d5daa | ||
|
|
3ce5a7da67 | ||
|
|
4d47b9c594 | ||
|
|
9f6eb142d2 | ||
|
|
0e08b4ee26 | ||
|
|
9b85907918 | ||
|
|
6463dca2c6 | ||
|
|
498b7fee69 | ||
|
|
3478e13d38 | ||
|
|
5f0d37739a | ||
|
|
c5b4f44ab8 | ||
|
|
819c9f61dc | ||
|
|
4f167feaf5 | ||
|
|
de558bc87c | ||
|
|
4a5a65ff6c | ||
|
|
c56e63d62f | ||
|
|
8cd9a3cabe | ||
|
|
3a8c324c12 | ||
|
|
ff882edeae | ||
|
|
fb0aa55cbb | ||
|
|
51015dc898 | ||
|
|
4af40e7861 | ||
|
|
24fcc0c3b0 | ||
|
|
993fc24dd3 | ||
|
|
fddc6bcd5f | ||
|
|
558051086e | ||
|
|
2c187bc55d | ||
|
|
e947979169 | ||
|
|
08f1ddb212 | ||
|
|
4c318d8d82 | ||
|
|
3e6ebfabb0 | ||
|
|
55f4692d99 | ||
|
|
ebe82cf3e6 | ||
|
|
21a8434e4d | ||
|
|
4990778a97 | ||
|
|
303e5c7996 | ||
|
|
599caee229 | ||
|
|
e6f28c6cdd | ||
|
|
fd3b0ee375 | ||
|
|
bd11ed9f17 | ||
|
|
a6a185004d | ||
|
|
3cc556d803 | ||
|
|
c3f9984346 | ||
|
|
10df4ee0d1 | ||
|
|
c03a183904 | ||
|
|
a2893fbec7 | ||
|
|
19cbace33d | ||
|
|
8a78481cca | ||
|
|
e1fd254d15 | ||
|
|
019219f1e1 | ||
|
|
ad3c04cb52 | ||
|
|
61f9dc7498 | ||
|
|
4deb16a37a | ||
|
|
4129151bd2 | ||
|
|
10cf431537 | ||
|
|
011dd2d973 | ||
|
|
c85c4c5020 | ||
|
|
5f1439df00 | ||
|
|
e76bec63a3 | ||
|
|
fc2b67aa0f | ||
|
|
bcd0360dd0 | ||
|
|
04bf2cd0c2 | ||
|
|
aba51da932 | ||
|
|
f8520d83be | ||
|
|
69003dfbe2 | ||
|
|
380b377ed8 | ||
|
|
4c5db983e3 | ||
|
|
48c887ac03 | ||
|
|
f207a82d2f | ||
|
|
56f6888d49 | ||
|
|
66ece479f6 | ||
|
|
c1cc2b064c | ||
|
|
98980b8192 | ||
|
|
79ec76f11f | ||
|
|
45a1c5c369 | ||
|
|
2dc41f319c | ||
|
|
2cdb1b8300 | ||
|
|
e846b4e20a | ||
|
|
961057f620 | ||
|
|
e686a09ce4 | ||
|
|
fc8cf2957f | ||
|
|
0bef37bfc1 | ||
|
|
1618141342 | ||
|
|
d7fb05f596 | ||
|
|
2eb15cc8e3 | ||
|
|
424a0233c2 | ||
|
|
40cf87307a | ||
|
|
643206b946 | ||
|
|
cc95041519 | ||
|
|
45b498f62f | ||
|
|
9e6d78ba5f | ||
|
|
95eba78d9c | ||
|
|
5d9f00b268 | ||
|
|
6a01388e82 | ||
|
|
2ef6f78d39 | ||
|
|
a754c39599 | ||
|
|
14622cd06c | ||
|
|
3132cd1198 | ||
|
|
94c35d86e2 | ||
|
|
3c2c6d782a | ||
|
|
1764b21214 | ||
|
|
260e572071 | ||
|
|
54251a27a8 | ||
|
|
88a8430c31 | ||
|
|
678b653873 | ||
|
|
21592ca5c0 | ||
|
|
1bca2f06bd | ||
|
|
9f166105a6 | ||
|
|
ea08b59e6b | ||
|
|
9aca0af22c | ||
|
|
591d8c3d1a | ||
|
|
22b73494a7 | ||
|
|
9bb80077c6 | ||
|
|
646f41663f |
@@ -1,3 +1,5 @@
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'com.google.protobuf'
|
||||
@@ -11,7 +13,7 @@ apply from: 'static-ips.gradle'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
|
||||
url "https://raw.githubusercontent.com/signalapp/maven/master/sqlcipher/release/"
|
||||
content {
|
||||
includeGroupByRegex "org\\.signal.*"
|
||||
}
|
||||
@@ -50,8 +52,8 @@ ktlint {
|
||||
version = "0.43.2"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1172
|
||||
def canonicalVersionName = "6.4.0"
|
||||
def canonicalVersionCode = 1183
|
||||
def canonicalVersionName = "6.6.1"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -93,10 +95,6 @@ android {
|
||||
freeCompilerArgs = ["-Xallow-result-return-type"]
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
javaMaxHeapSize "4g"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (keystores.debug != null) {
|
||||
debug {
|
||||
@@ -114,6 +112,17 @@ android {
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
|
||||
managedDevices {
|
||||
devices {
|
||||
pixel3api30 (ManagedVirtualDevice) {
|
||||
device = "Pixel 3"
|
||||
apiLevel = 30
|
||||
systemImageSource = "google-atd"
|
||||
require64Bit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@@ -165,7 +174,7 @@ android {
|
||||
multiDexEnabled true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
project.ext.set("archivesBaseName", "Signal");
|
||||
project.ext.set("archivesBaseName", "Signal")
|
||||
|
||||
manifestPlaceholders = [mapsKey:"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"]
|
||||
|
||||
@@ -575,7 +584,7 @@ def getLastCommitTimestamp() {
|
||||
}
|
||||
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
def result = exec {
|
||||
exec {
|
||||
executable = 'git'
|
||||
args = ['log', '-1', '--pretty=format:%ct']
|
||||
standardOutput = os
|
||||
@@ -587,20 +596,20 @@ def getLastCommitTimestamp() {
|
||||
|
||||
def getGitHash() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return "abcd1234"
|
||||
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
||||
}
|
||||
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||
commandLine 'git', 'rev-parse', 'HEAD'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
return stdout.toString().trim().substring(0, 12)
|
||||
}
|
||||
|
||||
def getCurrentGitTag() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return ''
|
||||
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
||||
}
|
||||
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
@@ -634,13 +643,13 @@ def loadKeystoreProperties(filename) {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
return keystoreProperties;
|
||||
return keystoreProperties
|
||||
} else {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
def getDateSuffix() {
|
||||
static def getDateSuffix() {
|
||||
def date = new Date()
|
||||
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
|
||||
return formattedDate
|
||||
|
||||
@@ -2,9 +2,11 @@ package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.FlakyTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -166,6 +168,8 @@ class ChangeNumberViewModelTest {
|
||||
* and apply the pending state after confirming the change on the server.
|
||||
*/
|
||||
@Test
|
||||
@FlakyTest
|
||||
@Ignore("Test sometimes requires manual intervention to continue.")
|
||||
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
|
||||
@@ -9,11 +9,8 @@ import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
|
||||
@@ -113,28 +110,15 @@ class ConversationItemPreviewer {
|
||||
}
|
||||
|
||||
val message = OutgoingMediaMessage(
|
||||
other,
|
||||
body,
|
||||
PointerAttachment.forPointers(Optional.of(attachments)),
|
||||
System.currentTimeMillis(),
|
||||
-1,
|
||||
0,
|
||||
false,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
null
|
||||
recipient = other,
|
||||
body = body,
|
||||
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
|
||||
timestamp = System.currentTimeMillis(),
|
||||
isSecure = true
|
||||
)
|
||||
|
||||
val insert = SignalDatabase.mms.insertMessageOutbox(
|
||||
OutgoingSecureMediaMessage(message),
|
||||
message,
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(other),
|
||||
false,
|
||||
null
|
||||
|
||||
@@ -7,7 +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.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
@@ -32,7 +32,7 @@ class SafetyNumberChangeDialogPreviewer {
|
||||
|
||||
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("Super really long name like omg", "But seriously it's long like really really long"))
|
||||
|
||||
harness.setVerified(other, IdentityDatabase.VerifiedStatus.VERIFIED)
|
||||
harness.setVerified(other, IdentityTable.VerifiedStatus.VERIFIED)
|
||||
harness.changeIdentityKey(other)
|
||||
|
||||
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
|
||||
@@ -52,7 +52,7 @@ class SafetyNumberChangeDialogPreviewer {
|
||||
othersRecipients.forEach { other ->
|
||||
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("My", "Name"))
|
||||
|
||||
harness.setVerified(other, IdentityDatabase.VerifiedStatus.DEFAULT)
|
||||
harness.setVerified(other, IdentityTable.VerifiedStatus.DEFAULT)
|
||||
harness.changeIdentityKey(other)
|
||||
|
||||
SignalDatabase.distributionLists.addMemberToList(DistributionListId.MY_STORY, DistributionListPrivacyMode.ONLY_WITH, other.id)
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.util.Optional
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AttachmentDatabaseTest {
|
||||
class AttachmentTableTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@@ -39,7 +39,7 @@ class AttachmentDatabaseTest {
|
||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||
val highQualityProperties = createHighQualityTransformProperties()
|
||||
val highQualityImage = createAttachment(1, blob, highQualityProperties)
|
||||
val lowQualityImage = createAttachment(1, blob, AttachmentDatabase.TransformProperties.empty())
|
||||
val lowQualityImage = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
|
||||
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
|
||||
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(lowQualityImage)
|
||||
|
||||
@@ -55,8 +55,8 @@ class AttachmentDatabaseTest {
|
||||
false
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentDatabase.DATA)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentDatabase.DATA)
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
@@ -81,13 +81,13 @@ class AttachmentDatabaseTest {
|
||||
true
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentDatabase.DATA)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentDatabase.DATA)
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
|
||||
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentDatabase.TransformProperties): UriAttachment {
|
||||
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties): UriAttachment {
|
||||
return UriAttachmentBuilder.build(
|
||||
id,
|
||||
uri = uri,
|
||||
@@ -96,8 +96,8 @@ class AttachmentDatabaseTest {
|
||||
)
|
||||
}
|
||||
|
||||
private fun createHighQualityTransformProperties(): AttachmentDatabase.TransformProperties {
|
||||
return AttachmentDatabase.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH)
|
||||
private fun createHighQualityTransformProperties(): AttachmentTable.TransformProperties {
|
||||
return AttachmentTable.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH)
|
||||
}
|
||||
|
||||
private fun createMediaStream(byteArray: ByteArray): MediaStream {
|
||||
@@ -10,9 +10,9 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import java.util.UUID
|
||||
|
||||
class DistributionListDatabaseTest {
|
||||
class DistributionListTablesTest {
|
||||
|
||||
private lateinit var distributionDatabase: DistributionListDatabase
|
||||
private lateinit var distributionDatabase: DistributionListTables
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@@ -5,7 +5,6 @@ import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.Optional
|
||||
|
||||
@@ -21,8 +20,8 @@ object MmsHelper {
|
||||
subscriptionId: Int = -1,
|
||||
expiresIn: Long = 0,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
threadId: Long = 1,
|
||||
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
|
||||
threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient, distributionType),
|
||||
storyType: StoryType = StoryType.NONE,
|
||||
parentStoryId: ParentStoryId? = null,
|
||||
isStoryReaction: Boolean = false,
|
||||
@@ -30,27 +29,19 @@ object MmsHelper {
|
||||
secure: Boolean = true
|
||||
): Long {
|
||||
val message = OutgoingMediaMessage(
|
||||
recipient,
|
||||
body,
|
||||
emptyList(),
|
||||
sentTimeMillis,
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
distributionType,
|
||||
storyType,
|
||||
parentStoryId,
|
||||
isStoryReaction,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
giftBadge
|
||||
).let {
|
||||
if (secure) OutgoingSecureMediaMessage(it) else it
|
||||
}
|
||||
recipient = recipient,
|
||||
body = body,
|
||||
timestamp = sentTimeMillis,
|
||||
subscriptionId = subscriptionId,
|
||||
expiresIn = expiresIn,
|
||||
viewOnce = viewOnce,
|
||||
distributionType = distributionType,
|
||||
storyType = storyType,
|
||||
parentStoryId = parentStoryId,
|
||||
isStoryReaction = isStoryReaction,
|
||||
giftBadge = giftBadge,
|
||||
isSecure = secure
|
||||
)
|
||||
|
||||
return insert(
|
||||
message = message,
|
||||
@@ -62,13 +53,13 @@ object MmsHelper {
|
||||
message: OutgoingMediaMessage,
|
||||
threadId: Long
|
||||
): Long {
|
||||
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
|
||||
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: IncomingMediaMessage,
|
||||
threadId: Long
|
||||
): Optional<MessageDatabase.InsertResult> {
|
||||
): Optional<MessageTable.InsertResult> {
|
||||
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MmsDatabaseTest_gifts {
|
||||
private lateinit var mms: MmsDatabase
|
||||
class MmsTableTest_gifts {
|
||||
private lateinit var mms: MmsTable
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
@@ -24,9 +24,9 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MmsDatabaseTest_stories {
|
||||
class MmsTableTest_stories {
|
||||
|
||||
private lateinit var mms: MmsDatabase
|
||||
private lateinit var mms: MmsTable
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
@@ -237,8 +237,7 @@ class MmsDatabaseTest_stories {
|
||||
MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = -1L
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
)
|
||||
|
||||
// WHEN
|
||||
@@ -296,8 +295,7 @@ class MmsDatabaseTest_stories {
|
||||
val groupStoryId = MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = -1L
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
@@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientDatabaseTest {
|
||||
class RecipientTableTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
@@ -38,7 +38,7 @@ class RecipientDatabaseTest {
|
||||
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(false)?.use {
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
@@ -79,7 +79,7 @@ class RecipientDatabaseTest {
|
||||
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(false)?.use {
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
@@ -109,7 +109,7 @@ class RecipientDatabaseTest {
|
||||
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(false)?.use {
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
@@ -150,7 +150,7 @@ class RecipientDatabaseTest {
|
||||
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(false)?.use {
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (it.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
@@ -39,7 +39,7 @@ import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientDatabaseTest_getAndPossiblyMerge {
|
||||
class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@@ -413,8 +413,8 @@ class RecipientDatabaseTest_getAndPossiblyMerge {
|
||||
val identityKeyAci: IdentityKey = identityKey(1)
|
||||
val identityKeyE164: IdentityKey = identityKey(2)
|
||||
|
||||
SignalDatabase.identities.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
|
||||
SignalDatabase.identities.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
|
||||
SignalDatabase.identities.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityTable.VerifiedStatus.VERIFIED, false, 0, false)
|
||||
SignalDatabase.identities.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityTable.VerifiedStatus.VERIFIED, false, 0, false)
|
||||
|
||||
SignalDatabase.sessions.store(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
|
||||
|
||||
@@ -485,7 +485,7 @@ class RecipientDatabaseTest_getAndPossiblyMerge {
|
||||
assertEquals(retrievedThreadId, mention2.threadId)
|
||||
|
||||
// Group receipt validation
|
||||
val groupReceipts: List<GroupReceiptDatabase.GroupReceiptInfo> = SignalDatabase.groupReceipts.getGroupReceiptInfo(mmsId1)
|
||||
val groupReceipts: List<GroupReceiptTable.GroupReceiptInfo> = SignalDatabase.groupReceipts.getGroupReceiptInfo(mmsId1)
|
||||
assertEquals(retrievedId, groupReceipts[0].recipientId)
|
||||
assertEquals(retrievedId, groupReceipts[1].recipientId)
|
||||
|
||||
@@ -539,11 +539,11 @@ class RecipientDatabaseTest_getAndPossiblyMerge {
|
||||
}
|
||||
|
||||
private fun getMention(messageId: Long): MentionModel {
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor ->
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionTable.TABLE_NAME} WHERE ${MentionTable.MESSAGE_ID} = $messageId").use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
return MentionModel(
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)),
|
||||
threadId = CursorUtil.requireLong(cursor, MentionDatabase.THREAD_ID)
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionTable.RECIPIENT_ID)),
|
||||
threadId = CursorUtil.requireLong(cursor, MentionTable.THREAD_ID)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -660,8 +660,8 @@ class RecipientDatabaseTest_getAndPossiblyMerge {
|
||||
fun expectDeleted(id: RecipientId) {
|
||||
SignalDatabase.rawDatabase
|
||||
.select("1")
|
||||
.from(RecipientDatabase.TABLE_NAME)
|
||||
.where("${RecipientDatabase.ID} = ?", id)
|
||||
.from(RecipientTable.TABLE_NAME)
|
||||
.where("${RecipientTable.ID} = ?", id)
|
||||
.run()
|
||||
.use { !it.moveToFirst() }
|
||||
}
|
||||
@@ -681,13 +681,13 @@ class RecipientDatabaseTest_getAndPossiblyMerge {
|
||||
val pniString: String? = pni?.toString()
|
||||
|
||||
val id: Long = SignalDatabase.rawDatabase.insert(
|
||||
RecipientDatabase.TABLE_NAME,
|
||||
RecipientTable.TABLE_NAME,
|
||||
null,
|
||||
contentValuesOf(
|
||||
RecipientDatabase.PHONE to e164,
|
||||
RecipientDatabase.SERVICE_ID to serviceIdString,
|
||||
RecipientDatabase.PNI_COLUMN to pniString,
|
||||
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
|
||||
RecipientTable.PHONE to e164,
|
||||
RecipientTable.SERVICE_ID to serviceIdString,
|
||||
RecipientTable.PNI_COLUMN to pniString,
|
||||
RecipientTable.REGISTERED to RecipientTable.RegisteredState.REGISTERED.id
|
||||
)
|
||||
)
|
||||
|
||||
@@ -18,13 +18,13 @@ import java.lang.IllegalStateException
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientDatabaseTest_processPnpTupleToChangeSet {
|
||||
class RecipientTableTest_processPnpTupleToChangeSet {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
|
||||
|
||||
private lateinit var db: RecipientDatabase
|
||||
private lateinit var db: RecipientTable
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@@ -711,13 +711,13 @@ class RecipientDatabaseTest_processPnpTupleToChangeSet {
|
||||
|
||||
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
|
||||
val id: Long = SignalDatabase.rawDatabase.insert(
|
||||
RecipientDatabase.TABLE_NAME,
|
||||
RecipientTable.TABLE_NAME,
|
||||
null,
|
||||
contentValuesOf(
|
||||
RecipientDatabase.PHONE to e164,
|
||||
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
|
||||
RecipientDatabase.PNI_COLUMN to pni?.toString(),
|
||||
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
|
||||
RecipientTable.PHONE to e164,
|
||||
RecipientTable.SERVICE_ID to (aci ?: pni)?.toString(),
|
||||
RecipientTable.PNI_COLUMN to pni?.toString(),
|
||||
RecipientTable.REGISTERED to RecipientTable.RegisteredState.REGISTERED.id
|
||||
)
|
||||
)
|
||||
|
||||
@@ -726,12 +726,12 @@ class RecipientDatabaseTest_processPnpTupleToChangeSet {
|
||||
|
||||
private fun insertMockSessionFor(account: ServiceId, address: ServiceId) {
|
||||
SignalDatabase.rawDatabase.insert(
|
||||
SessionDatabase.TABLE_NAME, null,
|
||||
SessionTable.TABLE_NAME, null,
|
||||
contentValuesOf(
|
||||
SessionDatabase.ACCOUNT_ID to account.toString(),
|
||||
SessionDatabase.ADDRESS to address.toString(),
|
||||
SessionDatabase.DEVICE to 1,
|
||||
SessionDatabase.RECORD to Util.getSecretBytes(32)
|
||||
SessionTable.ACCOUNT_ID to account.toString(),
|
||||
SessionTable.ADDRESS to address.toString(),
|
||||
SessionTable.DEVICE to 1,
|
||||
SessionTable.RECORD to Util.getSecretBytes(32)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -762,7 +762,7 @@ class RecipientDatabaseTest_processPnpTupleToChangeSet {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that will call insert your recipients, call [RecipientDatabase.processPnpTupleToChangeSet] with your params,
|
||||
* Helper method that will call insert your recipients, call [RecipientTable.processPnpTupleToChangeSet] with your params,
|
||||
* and then verify your output matches what you expect.
|
||||
*
|
||||
* It results the inserted ID's and changeset for additional verification.
|
||||
@@ -31,8 +31,8 @@ import java.util.UUID
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
|
||||
private lateinit var recipients: RecipientDatabase
|
||||
private lateinit var sms: SmsDatabase
|
||||
private lateinit var recipients: RecipientTable
|
||||
private lateinit var sms: SmsTable
|
||||
|
||||
private val localAci = ACI.from(UUID.randomUUID())
|
||||
private val localPni = PNI.from(UUID.randomUUID())
|
||||
@@ -163,7 +163,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
*/
|
||||
@Test
|
||||
fun previousJoinRequestCollapse() {
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
@@ -197,7 +197,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
fun previousJoinThenTextCollapse() {
|
||||
val secondLatestMessage = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get()
|
||||
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
@@ -231,7 +231,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
*/
|
||||
@Test
|
||||
fun previousCollapseAndJoinRequestDoubleCollapse() {
|
||||
val secondLatestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
val secondLatestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
@@ -243,7 +243,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
)
|
||||
).get()
|
||||
|
||||
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
|
||||
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
|
||||
groupUpdateMessage(
|
||||
sender = alice,
|
||||
groupContext = groupContext(masterKey = masterKey) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -22,7 +23,7 @@ import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StorySendsDatabaseTest {
|
||||
class StorySendTableTest {
|
||||
|
||||
private val distributionId1 = DistributionId.from(UUID.randomUUID())
|
||||
private val distributionId2 = DistributionId.from(UUID.randomUUID())
|
||||
@@ -45,7 +46,7 @@ class StorySendsDatabaseTest {
|
||||
private var messageId2: Long = 0
|
||||
private var messageId3: Long = 0
|
||||
|
||||
private lateinit var storySends: StorySendsDatabase
|
||||
private lateinit var storySends: StorySendTable
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@@ -287,6 +288,7 @@ class StorySendsDatabaseTest {
|
||||
assertNotNull(manifest)
|
||||
}
|
||||
|
||||
/*
|
||||
@Test
|
||||
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetRecipientIdsForManifestUpdate_thenIExpectOnlyRecipientsWithStory2() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
@@ -324,7 +326,7 @@ class StorySendsDatabaseTest {
|
||||
|
||||
assertTrue(results.entries.all { it.allowedToReply })
|
||||
}
|
||||
|
||||
*/
|
||||
@Test
|
||||
fun givenEmptyManifest_whenIApplyRemoteManifest_thenNothingChanges() {
|
||||
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
|
||||
@@ -354,8 +356,8 @@ class StorySendsDatabaseTest {
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeMarkedRemoteDeleted() {
|
||||
@Test(expected = NoSuchMessageException::class)
|
||||
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeDeleted() {
|
||||
val messageId4 = MmsHelper.insert(
|
||||
recipient = distributionListRecipient1,
|
||||
storyType = StoryType.STORY_WITHOUT_REPLIES,
|
||||
@@ -375,7 +377,8 @@ class StorySendsDatabaseTest {
|
||||
|
||||
storySends.applySentStoryManifest(remote, 200)
|
||||
|
||||
assertTrue(SignalDatabase.mms.getMessageRecord(messageId5).isRemoteDelete)
|
||||
SignalDatabase.mms.getMessageRecord(messageId5)
|
||||
fail("Expected messageId5 to no longer exist.")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -6,13 +6,14 @@ import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
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 java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
class ThreadDatabaseTest_pinned {
|
||||
class ThreadTableTest_pinned {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
@@ -51,7 +52,7 @@ class ThreadDatabaseTest_pinned {
|
||||
SignalDatabase.mms.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
val unarchivedCount = SignalDatabase.threads.getUnarchivedConversationListCount()
|
||||
val unarchivedCount = SignalDatabase.threads.getUnarchivedConversationListCount(ConversationFilter.OFF)
|
||||
assertEquals(1, unarchivedCount)
|
||||
}
|
||||
|
||||
@@ -66,9 +67,9 @@ class ThreadDatabaseTest_pinned {
|
||||
SignalDatabase.mms.deleteMessage(messageId)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.threads.getUnarchivedConversationList(true, 0, 1).use {
|
||||
SignalDatabase.threads.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 1).use {
|
||||
it.moveToFirst()
|
||||
assertEquals(threadId, CursorUtil.requireLong(it, ThreadDatabase.ID))
|
||||
assertEquals(threadId, CursorUtil.requireLong(it, ThreadTable.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ThreadDatabaseTest_recents {
|
||||
class ThreadTableTest_recents {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
@@ -40,7 +40,7 @@ class ThreadDatabaseTest_recents {
|
||||
val results: MutableList<RecipientId> = SignalDatabase.threads.getRecentConversationList(10, false, false, false, false, false, false).use { cursor ->
|
||||
val ids = mutableListOf<RecipientId>()
|
||||
while (cursor.moveToNext()) {
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(cursor, ThreadDatabase.RECIPIENT_ID)))
|
||||
ids.add(RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)))
|
||||
}
|
||||
|
||||
ids
|
||||
@@ -11,7 +11,7 @@ object UriAttachmentBuilder {
|
||||
id: Long,
|
||||
uri: Uri = Uri.parse("content://$id"),
|
||||
contentType: String,
|
||||
transferState: Int = AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
|
||||
transferState: Int = AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
size: Long = 0L,
|
||||
fileName: String = "file$id",
|
||||
voiceNote: Boolean = false,
|
||||
@@ -22,7 +22,7 @@ object UriAttachmentBuilder {
|
||||
stickerLocator: StickerLocator? = null,
|
||||
blurHash: BlurHash? = null,
|
||||
audioHash: AudioHash? = null,
|
||||
transformProperties: AttachmentDatabase.TransformProperties? = null
|
||||
transformProperties: AttachmentTable.TransformProperties? = null
|
||||
): UriAttachment {
|
||||
return UriAttachment(
|
||||
uri,
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.thoughtcrime.securesms.database.DistributionListDatabase
|
||||
import org.thoughtcrime.securesms.database.DistributionListTables
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
@@ -72,9 +72,9 @@ class MyStoryMigrationTest {
|
||||
|
||||
private fun setMyStoryDistributionId(serializedId: String) {
|
||||
SignalDatabase.rawDatabase.update(
|
||||
DistributionListDatabase.LIST_TABLE_NAME,
|
||||
DistributionListTables.LIST_TABLE_NAME,
|
||||
contentValuesOf(
|
||||
DistributionListDatabase.DISTRIBUTION_ID to serializedId
|
||||
DistributionListTables.DISTRIBUTION_ID to serializedId
|
||||
),
|
||||
"_id = ?",
|
||||
SqlUtil.buildArgs(DistributionListId.MY_STORY)
|
||||
@@ -83,7 +83,7 @@ class MyStoryMigrationTest {
|
||||
|
||||
private fun deleteMyStory() {
|
||||
SignalDatabase.rawDatabase.delete(
|
||||
DistributionListDatabase.LIST_TABLE_NAME,
|
||||
DistributionListTables.LIST_TABLE_NAME,
|
||||
"_id = ?",
|
||||
SqlUtil.buildArgs(DistributionListId.MY_STORY)
|
||||
)
|
||||
@@ -91,9 +91,9 @@ class MyStoryMigrationTest {
|
||||
|
||||
private fun assertValidMyStoryExists() {
|
||||
SignalDatabase.rawDatabase.query(
|
||||
DistributionListDatabase.LIST_TABLE_NAME,
|
||||
DistributionListTables.LIST_TABLE_NAME,
|
||||
SqlUtil.COUNT,
|
||||
"_id = ? AND ${DistributionListDatabase.DISTRIBUTION_ID} = ?",
|
||||
"_id = ? AND ${DistributionListTables.DISTRIBUTION_ID} = ?",
|
||||
SqlUtil.buildArgs(DistributionListId.MY_STORY, DistributionId.MY_STORY.toString()),
|
||||
null,
|
||||
null,
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MmsHelper
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
@@ -74,7 +74,7 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
|
||||
|
||||
val replyId = SignalDatabase.mmsSms.getConversation(senderThreadId, 0, 1).use {
|
||||
it.moveToFirst()
|
||||
it.requireLong(MessageDatabase.ID)
|
||||
it.requireLong(MessageTable.ID)
|
||||
}
|
||||
|
||||
val replyRecord = SignalDatabase.mms.getMessageRecord(replyId) as MediaMmsMessageRecord
|
||||
@@ -140,7 +140,7 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
|
||||
val replyId = SignalDatabase.mms.getStoryReplies(insertResult.get().messageId).use { cursor ->
|
||||
assertEquals(1, cursor.count)
|
||||
cursor.moveToFirst()
|
||||
cursor.requireLong(MessageDatabase.ID)
|
||||
cursor.requireLong(MessageTable.ID)
|
||||
}
|
||||
|
||||
val replyRecord = SignalDatabase.mms.getMessageRecord(replyId) as MediaMmsMessageRecord
|
||||
@@ -176,6 +176,6 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
|
||||
private fun runTestWithContent(contentProto: SignalServiceContentProto) {
|
||||
val content = SignalServiceContent.createFromProto(contentProto)
|
||||
val testSubject = createNormalContentTestSubject()
|
||||
testSubject.doProcess(content = content)
|
||||
testSubject.doProcess(content = content!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class MessageContentProcessor__handleTextMessageTest : MessageContentProcessorTe
|
||||
val content = SignalServiceContent.createFromProto(contentProto)
|
||||
|
||||
// WHEN
|
||||
testSubject.doProcess(content = content)
|
||||
testSubject.doProcess(content = content!!)
|
||||
|
||||
// THEN
|
||||
val record = SignalDatabase.sms.getMessageRecord(1)
|
||||
|
||||
@@ -73,6 +73,7 @@ class UsernameEditFragmentTest {
|
||||
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||
}
|
||||
|
||||
@Ignore("Flakey espresso test.")
|
||||
@Test
|
||||
fun testUsernameCreationOutsideOfRegistration() {
|
||||
val scenario = createScenario()
|
||||
|
||||
@@ -33,7 +33,7 @@ class SafetyNumberBottomSheetRepositoryTest {
|
||||
|
||||
testScheduler.triggerActions()
|
||||
|
||||
result.assertValueAt(1) { map ->
|
||||
result.assertValueAt(0) { map ->
|
||||
assertMatch(map, mapOf(SafetyNumberBucket.ContactsBucket to harness.others))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ object MockProvider {
|
||||
}
|
||||
|
||||
kbsRepository.stub {
|
||||
on { getToken(any() as String) } doReturn Single.just(ServiceResponse.forResult(tokenData, 200, ""))
|
||||
on { getToken(any() as? String) } doReturn Single.just(ServiceResponse.forResult(tokenData, 200, ""))
|
||||
}
|
||||
|
||||
val session: KeyBackupService.RestoreSession = object : KeyBackupService.RestoreSession {
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
@@ -130,7 +130,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
|
||||
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
|
||||
}
|
||||
|
||||
fun setVerified(recipient: Recipient, status: IdentityDatabase.VerifiedStatus) {
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityDatabase.VerifiedStatus.VERIFIED)
|
||||
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,11 +332,6 @@
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
|
||||
<activity android:name=".DatabaseMigrationActivity"
|
||||
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".migrations.ApplicationMigrationActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
@@ -702,7 +697,6 @@
|
||||
|
||||
<service android:enabled="true" android:name=".exporter.SignalSmsExportService" android:foregroundServiceType="dataSync" />
|
||||
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService" android:foregroundServiceType="camera|microphone"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
|
||||
<service android:name=".service.webrtc.AndroidCallConnectionService"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
@@ -35,6 +36,8 @@ import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.AndroidLogger;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.donations.GooglePayApi;
|
||||
import org.signal.donations.StripeApi;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
@@ -89,6 +92,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Environment;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
@@ -102,6 +106,8 @@ import java.net.SocketTimeoutException;
|
||||
import java.security.Security;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.rxjava3.core.CompletableObserver;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
@@ -146,7 +152,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
SignalDatabase.init(this,
|
||||
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
|
||||
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
|
||||
SignalDatabase.triggerDatabaseAccess();
|
||||
})
|
||||
.addBlocking("logging", () -> {
|
||||
initializeLogging();
|
||||
@@ -177,6 +182,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addBlocking("blob-provider", this::initializeBlobProvider)
|
||||
.addBlocking("feature-flags", FeatureFlags::init)
|
||||
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
|
||||
.addNonBlocking(this::checkIsGooglePayReady)
|
||||
.addNonBlocking(this::cleanAvatarStorage)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
@@ -460,6 +466,18 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
AvatarPickerStorage.cleanOrphans(this);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void checkIsGooglePayReady() {
|
||||
GooglePayApi.queryIsReadyToPay(
|
||||
this,
|
||||
new StripeApi.Gateway(Environment.Donations.getStripeConfiguration()),
|
||||
Environment.Donations.getGooglePayConfiguration()
|
||||
).subscribe(
|
||||
/* onComplete = */ () -> SignalStore.donationsValues().setGooglePayReady(true),
|
||||
/* onError = */ t -> SignalStore.donationsValues().setGooglePayReady(false)
|
||||
);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void initializeCleanup() {
|
||||
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.Parcelable;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService.ImportState;
|
||||
|
||||
public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
|
||||
|
||||
private final ImportServiceConnection serviceConnection = new ImportServiceConnection();
|
||||
private final ImportStateHandler importStateHandler = new ImportStateHandler();
|
||||
private final BroadcastReceiver completedReceiver = new NullReceiver();
|
||||
|
||||
private LinearLayout promptLayout;
|
||||
private LinearLayout progressLayout;
|
||||
private Button skipButton;
|
||||
private Button importButton;
|
||||
private ProgressBar progress;
|
||||
private TextView progressLabel;
|
||||
|
||||
private ApplicationMigrationService importService;
|
||||
private boolean isVisible = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
setContentView(R.layout.database_migration_activity);
|
||||
|
||||
initializeResources();
|
||||
initializeServiceBinding();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
isVisible = true;
|
||||
registerForCompletedNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
isVisible = false;
|
||||
unregisterForCompletedNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
shutdownServiceBinding();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
|
||||
}
|
||||
|
||||
private void initializeServiceBinding() {
|
||||
Intent intent = new Intent(this, ApplicationMigrationService.class);
|
||||
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
this.promptLayout = (LinearLayout)findViewById(R.id.prompt_layout);
|
||||
this.progressLayout = (LinearLayout)findViewById(R.id.progress_layout);
|
||||
this.skipButton = (Button) findViewById(R.id.skip_button);
|
||||
this.importButton = (Button) findViewById(R.id.import_button);
|
||||
this.progress = (ProgressBar) findViewById(R.id.import_progress);
|
||||
this.progressLabel = (TextView) findViewById(R.id.import_status);
|
||||
|
||||
this.progressLayout.setVisibility(View.GONE);
|
||||
this.promptLayout.setVisibility(View.GONE);
|
||||
|
||||
this.importButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(DatabaseMigrationActivity.this, ApplicationMigrationService.class);
|
||||
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
|
||||
intent.putExtra("master_secret", (Parcelable)getIntent().getParcelableExtra("master_secret"));
|
||||
startService(intent);
|
||||
|
||||
promptLayout.setVisibility(View.GONE);
|
||||
progressLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
|
||||
this.skipButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ApplicationMigrationService.setDatabaseImported(DatabaseMigrationActivity.this);
|
||||
handleImportComplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void registerForCompletedNotification() {
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ApplicationMigrationService.COMPLETED_ACTION);
|
||||
filter.setPriority(1000);
|
||||
|
||||
registerReceiver(completedReceiver, filter);
|
||||
}
|
||||
|
||||
private void unregisterForCompletedNotification() {
|
||||
unregisterReceiver(completedReceiver);
|
||||
}
|
||||
|
||||
private void shutdownServiceBinding() {
|
||||
unbindService(serviceConnection);
|
||||
}
|
||||
|
||||
private void handleStateIdle() {
|
||||
this.promptLayout.setVisibility(View.VISIBLE);
|
||||
this.progressLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void handleStateProgress(ProgressDescription update) {
|
||||
this.promptLayout.setVisibility(View.GONE);
|
||||
this.progressLayout.setVisibility(View.VISIBLE);
|
||||
this.progressLabel.setText(update.primaryComplete + "/" + update.primaryTotal);
|
||||
|
||||
double max = this.progress.getMax();
|
||||
double primaryTotal = update.primaryTotal;
|
||||
double primaryComplete = update.primaryComplete;
|
||||
double secondaryTotal = update.secondaryTotal;
|
||||
double secondaryComplete = update.secondaryComplete;
|
||||
|
||||
this.progress.setProgress((int)Math.round((primaryComplete / primaryTotal) * max));
|
||||
this.progress.setSecondaryProgress((int)Math.round((secondaryComplete / secondaryTotal) * max));
|
||||
}
|
||||
|
||||
private void handleImportComplete() {
|
||||
if (isVisible) {
|
||||
if (getIntent().hasExtra("next_intent")) {
|
||||
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
|
||||
} else {
|
||||
// TODO [greyson] Navigation
|
||||
startActivity(MainActivity.clearTop(this));
|
||||
}
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private class ImportStateHandler extends Handler {
|
||||
|
||||
public ImportStateHandler() {
|
||||
super(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
switch (message.what) {
|
||||
case ImportState.STATE_IDLE: handleStateIdle(); break;
|
||||
case ImportState.STATE_MIGRATING_IN_PROGRESS: handleStateProgress((ProgressDescription)message.obj); break;
|
||||
case ImportState.STATE_MIGRATING_COMPLETE: handleImportComplete(); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ImportServiceConnection implements ServiceConnection {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
importService = ((ApplicationMigrationService.ApplicationMigrationBinder)service).getService();
|
||||
importService.setImportStateHandler(importStateHandler);
|
||||
|
||||
ImportState state = importService.getState();
|
||||
importStateHandler.obtainMessage(state.state, state.progress).sendToTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
importService.setImportStateHandler(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class NullReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
abortBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -602,7 +602,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
public void handleGroupMemberCountChange(int count) {
|
||||
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize() && FeatureFlags.groupCallRinging();
|
||||
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize();
|
||||
callScreen.enableRingGroup(canRing);
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public class PointerAttachment extends Attachment {
|
||||
return results;
|
||||
}
|
||||
|
||||
public static List<Attachment> forPointers(List<SignalServiceDataMessage.Quote.QuotedAttachment> pointers) {
|
||||
public static List<Attachment> forPointers(@Nullable List<SignalServiceDataMessage.Quote.QuotedAttachment> pointers) {
|
||||
List<Attachment> results = new LinkedList<>();
|
||||
|
||||
if (pointers != null) {
|
||||
|
||||
@@ -51,6 +51,7 @@ private class AudioRecorderFocusManager26(context: Context, changeListener: OnAu
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private class AudioRecorderFocusManagerLegacy(context: Context, val changeListener: OnAudioFocusChangeListener) : AudioRecorderFocusManager(context) {
|
||||
override fun requestAudioFocus(): Int {
|
||||
return audioManager.requestAudioFocus(changeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
|
||||
|
||||
@@ -30,6 +30,7 @@ class TextAvatarDrawable(
|
||||
setBounds(0, 0, size, size)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = bounds.width()
|
||||
val textSize = Avatars.getTextSizeForLength(context, avatar.text, width * 0.8f, width * 0.45f)
|
||||
|
||||
@@ -42,7 +42,7 @@ public enum BackupFileIOError {
|
||||
|
||||
public void postNotification(@NonNull Context context) {
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), PendingIntentFlags.mutable());
|
||||
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
|
||||
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
|
||||
.setSmallIcon(R.drawable.ic_signal_backup)
|
||||
.setContentTitle(context.getString(titleId))
|
||||
.setContentText(context.getString(messageId))
|
||||
|
||||
@@ -48,7 +48,7 @@ object BackupVerifier {
|
||||
try {
|
||||
inputStream.readAttachmentTo(NullOutputStream, attachment.length)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Bad attachment: ${attachment.attachmentId}", e)
|
||||
Log.w(TAG, "Bad attachment id: ${attachment.attachmentId} len: ${attachment.length}", e)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ object BackupVerifier {
|
||||
try {
|
||||
inputStream.readAttachmentTo(NullOutputStream, sticker.length)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Bad sticker: ${sticker.rowId}", e)
|
||||
Log.w(TAG, "Bad sticker id: ${sticker.rowId} len: ${sticker.length}", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -69,7 +69,7 @@ object BackupVerifier {
|
||||
try {
|
||||
inputStream.readAttachmentTo(NullOutputStream, avatar.length)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Bad sticker: ${avatar.recipientId}", e)
|
||||
Log.w(TAG, "Bad avatar id: ${avatar.recipientId} len: ${avatar.length}", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -473,7 +473,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) {
|
||||
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w(TAG, "Missing attachment: " + e.getMessage());
|
||||
Log.w(TAG, "Missing attachment", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +498,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
|
||||
outputStream.writeSticker(rowId, inputStream, size);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w(TAG, "Missing sticker: " + e.getMessage());
|
||||
Log.w(TAG, "Missing sticker", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +516,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
result += read;
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w(TAG, "Missing attachment: " + e.getMessage());
|
||||
Log.w(TAG, "Missing attachment for size calculation", e);
|
||||
return 0;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to determine stream length", e);
|
||||
|
||||
@@ -5,11 +5,11 @@ import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
@@ -42,29 +42,24 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class FullBackupImporter extends FullBackupBase {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(FullBackupImporter.class);
|
||||
|
||||
private static final String[] TABLES_TO_DROP_FIRST = {
|
||||
"distribution_list_member",
|
||||
"distribution_list",
|
||||
"message_send_log_recipients",
|
||||
"msl_recipient",
|
||||
"msl_message",
|
||||
"reaction",
|
||||
"notification_profile_schedule",
|
||||
"notification_profile_allowed_members",
|
||||
"story_sends"
|
||||
};
|
||||
|
||||
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
|
||||
throws IOException
|
||||
@@ -268,21 +263,69 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
}
|
||||
|
||||
private static void dropAllTables(@NonNull SQLiteDatabase db) {
|
||||
for (String name : TABLES_TO_DROP_FIRST) {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + name);
|
||||
for (String trigger : SqlUtil.getAllTriggers(db)) {
|
||||
Log.i(TAG, "Dropping trigger: " + trigger);
|
||||
db.execSQL("DROP TRIGGER IF EXISTS " + trigger);
|
||||
}
|
||||
for (String table : getTablesToDropInOrder(db)) {
|
||||
Log.i(TAG, "Dropping table: " + table);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + table);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of tables we should drop, in the order they should be dropped in.
|
||||
* The order is chosen to ensure we won't violate any foreign key constraints when we import them.
|
||||
*/
|
||||
private static List<String> getTablesToDropInOrder(@NonNull SQLiteDatabase input) {
|
||||
List<String> tables = SqlUtil.getAllTables(input)
|
||||
.stream()
|
||||
.filter(table -> !table.startsWith("sqlite_"))
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
|
||||
Map<String, Set<String>> dependsOn = new LinkedHashMap<>();
|
||||
for (String table : tables) {
|
||||
dependsOn.put(table, SqlUtil.getForeignKeyDependencies(input, table));
|
||||
}
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT name, type FROM sqlite_master", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String name = cursor.getString(0);
|
||||
String type = cursor.getString(1);
|
||||
for (String table : tables) {
|
||||
Set<String> dependsOnTable = dependsOn.keySet().stream().filter(t -> dependsOn.get(t).contains(table)).collect(Collectors.toSet());
|
||||
Log.i(TAG, "Tables that depend on " + table + ": " + dependsOnTable);
|
||||
}
|
||||
|
||||
if ("table".equals(type) && !name.startsWith("sqlite_")) {
|
||||
Log.i(TAG, "Dropping table: " + name);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + name);
|
||||
}
|
||||
return computeTableDropOrder(dependsOn);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static List<String> computeTableDropOrder(@NonNull Map<String, Set<String>> dependsOn) {
|
||||
List<String> rootNodes = dependsOn.keySet()
|
||||
.stream()
|
||||
.filter(table -> {
|
||||
boolean nothingDependsOnIt = dependsOn.values().stream().noneMatch(it -> it.contains(table));
|
||||
return nothingDependsOnIt;
|
||||
})
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
LinkedHashSet<String> dropOrder = new LinkedHashSet<>();
|
||||
|
||||
Queue<String> processOrder = new LinkedList<>(rootNodes);
|
||||
|
||||
while (!processOrder.isEmpty()) {
|
||||
String head = processOrder.remove();
|
||||
|
||||
dropOrder.remove(head);
|
||||
dropOrder.add(head);
|
||||
|
||||
Set<String> dependencies = dependsOn.get(head);
|
||||
if (dependencies != null) {
|
||||
processOrder.addAll(dependencies);
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(dropOrder);
|
||||
}
|
||||
|
||||
public static class DatabaseDowngradeException extends IOException {
|
||||
|
||||
@@ -4,11 +4,8 @@ import android.content.Context
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import java.lang.Integer.min
|
||||
@@ -33,22 +30,13 @@ object Gifts {
|
||||
sentTimestamp: Long,
|
||||
expiresIn: Long
|
||||
): OutgoingMediaMessage {
|
||||
return OutgoingSecureMediaMessage(
|
||||
recipient,
|
||||
Base64.encodeBytes(giftBadge.toByteArray()),
|
||||
listOf(),
|
||||
sentTimestamp,
|
||||
ThreadTable.DistributionTypes.CONVERSATION,
|
||||
expiresIn,
|
||||
false,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
listOf(),
|
||||
listOf(),
|
||||
listOf(),
|
||||
giftBadge
|
||||
return OutgoingMediaMessage(
|
||||
recipient = recipient,
|
||||
body = Base64.encodeBytes(giftBadge.toByteArray()),
|
||||
isSecure = true,
|
||||
sentTimeMillis = sentTimestamp,
|
||||
expiresIn = expiresIn,
|
||||
giftBadge = giftBadge
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class GiftFlowActivity : FragmentWrapperActivity(), DonationPaymentComponent {
|
||||
return NavHostFragment.create(R.navigation.gift_flow)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.badges.gifts.flow
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.view.KeyEvent
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
@@ -26,8 +25,6 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.Do
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
|
||||
@@ -72,7 +69,6 @@ class GiftFlowConfirmationFragment :
|
||||
private lateinit var emojiKeyboard: MediaKeyboard
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private var errorDialog: DialogInterface? = null
|
||||
private var donationCheckoutDelegate: DonationCheckoutDelegate? = null
|
||||
private lateinit var processingDonationPaymentDialog: AlertDialog
|
||||
private lateinit var verifyingRecipientDonationPaymentDialog: AlertDialog
|
||||
@@ -87,7 +83,7 @@ class GiftFlowConfirmationFragment :
|
||||
|
||||
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
|
||||
|
||||
donationCheckoutDelegate = DonationCheckoutDelegate(this, this)
|
||||
donationCheckoutDelegate = DonationCheckoutDelegate(this, this, DonationErrorSource.GIFT)
|
||||
|
||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.processing_payment_dialog)
|
||||
@@ -161,7 +157,7 @@ class GiftFlowConfirmationFragment :
|
||||
viewModel.setAdditionalMessage(it)
|
||||
},
|
||||
onEmojiToggleClicked = {
|
||||
if (inputAwareLayout.isKeyboardOpen || (!inputAwareLayout.isKeyboardOpen && !inputAwareLayout.isInputOpen)) {
|
||||
if ((inputAwareLayout.isKeyboardOpen && !emojiKeyboard.isEmojiSearchMode) || (!inputAwareLayout.isKeyboardOpen && !inputAwareLayout.isInputOpen)) {
|
||||
inputAwareLayout.show(it, emojiKeyboard)
|
||||
emojiToggle.setImageResource(R.drawable.ic_keyboard_24)
|
||||
} else {
|
||||
@@ -192,12 +188,6 @@ class GiftFlowConfirmationFragment :
|
||||
}
|
||||
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
lifecycleDisposable += DonationError
|
||||
.getErrorsForSource(DonationErrorSource.GIFT)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { donationError ->
|
||||
onPaymentError(donationError)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
@@ -236,24 +226,6 @@ class GiftFlowConfirmationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPaymentError(throwable: Throwable?) {
|
||||
Log.w(TAG, "onPaymentError", throwable, true)
|
||||
|
||||
if (errorDialog != null) {
|
||||
Log.i(TAG, "Already displaying an error dialog. Skipping.")
|
||||
return
|
||||
}
|
||||
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
requireContext(), throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
override fun onDialogDismissed() {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onToolbarNavigationClicked() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
@@ -301,4 +273,7 @@ class GiftFlowConfirmationFragment :
|
||||
}
|
||||
|
||||
override fun onProcessorActionProcessed() = Unit
|
||||
override fun onUserCancelledPaymentFlow() {
|
||||
findNavController().popBackStack(R.id.giftFlowConfirmationFragment, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadgeAmounts
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadges
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.DonationsConfiguration
|
||||
import java.io.IOException
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
@@ -29,15 +29,14 @@ class GiftFlowRepository {
|
||||
private val TAG = Log.tag(GiftFlowRepository::class.java)
|
||||
}
|
||||
|
||||
fun getGiftBadge(): Single<Pair<Long, Badge>> {
|
||||
fun getGiftBadge(): Single<Pair<Int, Badge>> {
|
||||
return Single
|
||||
.fromCallable {
|
||||
ApplicationDependencies.getDonationsService()
|
||||
.getGiftBadges(Locale.getDefault())
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.flatMap(ServiceResponse<Map<Long, SignalServiceProfile.Badge>>::flattenResult)
|
||||
.map { gifts -> gifts.map { it.key to Badges.fromServiceBadge(it.value) } }
|
||||
.map { it.first() }
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { DonationsConfiguration.GIFT_LEVEL to it.getGiftBadges().first() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@@ -45,20 +44,17 @@ class GiftFlowRepository {
|
||||
return Single
|
||||
.fromCallable {
|
||||
ApplicationDependencies.getDonationsService()
|
||||
.giftAmount
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { result ->
|
||||
result
|
||||
.filter { PlatformCurrencyUtil.getAvailableCurrencyCodes().contains(it.key) }
|
||||
.mapKeys { (code, _) -> Currency.getInstance(code) }
|
||||
.mapValues { (currency, price) -> FiatMoney(price, currency) }
|
||||
}
|
||||
.map { it.getGiftBadgeAmounts() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the given recipient is a supported target for a gift.
|
||||
*
|
||||
* TODO[alex] - this needs to be incorporated into the correct flows.
|
||||
*/
|
||||
fun verifyRecipientIsAllowedToReceiveAGift(badgeRecipient: RecipientId): Completable {
|
||||
return Completable.fromAction {
|
||||
|
||||
@@ -83,7 +83,7 @@ class GiftFlowViewModel(
|
||||
onSuccess = { (giftLevel, giftBadge) ->
|
||||
store.update {
|
||||
it.copy(
|
||||
giftLevel = giftLevel,
|
||||
giftLevel = giftLevel.toLong(),
|
||||
giftBadge = giftBadge,
|
||||
stage = getLoadState(it, giftBadge = giftBadge)
|
||||
)
|
||||
|
||||
@@ -4,8 +4,8 @@ import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getBadge
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
@@ -23,10 +23,10 @@ class ViewGiftRepository {
|
||||
.fromCallable {
|
||||
ApplicationDependencies
|
||||
.getDonationsService()
|
||||
.getGiftBadge(Locale.getDefault(), presentation.receiptLevel)
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { Badges.fromServiceBadge(it) }
|
||||
.map { it.getBadge(presentation.receiptLevel.toInt()) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
@@ -37,10 +38,13 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
|
||||
private BlockedUsersViewModel viewModel;
|
||||
|
||||
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
super.onCreate(savedInstanceState, ready);
|
||||
|
||||
lifecycleDisposable.bindTo(this);
|
||||
dynamicTheme.onCreate(this);
|
||||
|
||||
setContentView(R.layout.blocked_users_activity);
|
||||
@@ -78,7 +82,11 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
|
||||
.add(R.id.fragment_container, new BlockedUsersFragment())
|
||||
.commit();
|
||||
|
||||
viewModel.getEvents().observe(this, event -> handleEvent(container, event));
|
||||
lifecycleDisposable.add(
|
||||
viewModel
|
||||
.getEvents()
|
||||
.subscribe(event -> handleEvent(container, event))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,12 +15,15 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import org.thoughtcrime.securesms.BlockUnblockDialog;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
|
||||
public class BlockedUsersFragment extends Fragment {
|
||||
|
||||
private BlockedUsersViewModel viewModel;
|
||||
private Listener listener;
|
||||
|
||||
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
@@ -59,16 +62,19 @@ public class BlockedUsersFragment extends Fragment {
|
||||
}
|
||||
});
|
||||
|
||||
lifecycleDisposable.bindTo(getViewLifecycleOwner());
|
||||
viewModel = new ViewModelProvider(requireActivity()).get(BlockedUsersViewModel.class);
|
||||
viewModel.getRecipients().observe(getViewLifecycleOwner(), list -> {
|
||||
if (list.isEmpty()) {
|
||||
empty.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
empty.setVisibility(View.GONE);
|
||||
}
|
||||
lifecycleDisposable.add(
|
||||
viewModel.getRecipients().subscribe(list -> {
|
||||
if (list.isEmpty()) {
|
||||
empty.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
empty.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
adapter.submitList(list);
|
||||
});
|
||||
adapter.submitList(list);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private void handleRecipientClicked(@NonNull Recipient recipient) {
|
||||
|
||||
@@ -2,64 +2,66 @@ package org.thoughtcrime.securesms.blocked;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject;
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject;
|
||||
import io.reactivex.rxjava3.subjects.Subject;
|
||||
|
||||
public class BlockedUsersViewModel extends ViewModel {
|
||||
|
||||
private final BlockedUsersRepository repository;
|
||||
private final MutableLiveData<List<Recipient>> recipients;
|
||||
private final SingleLiveEvent<Event> events = new SingleLiveEvent<>();
|
||||
private final BlockedUsersRepository repository;
|
||||
private final Subject<List<Recipient>> recipients = BehaviorSubject.create();
|
||||
private final Subject<Event> events = PublishSubject.create();
|
||||
|
||||
private BlockedUsersViewModel(@NonNull BlockedUsersRepository repository) {
|
||||
this.repository = repository;
|
||||
this.recipients = new MutableLiveData<>();
|
||||
|
||||
loadRecipients();
|
||||
}
|
||||
|
||||
public LiveData<List<Recipient>> getRecipients() {
|
||||
return recipients;
|
||||
public Observable<List<Recipient>> getRecipients() {
|
||||
return recipients.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public LiveData<Event> getEvents() {
|
||||
return events;
|
||||
public Observable<Event> getEvents() {
|
||||
return events.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
void block(@NonNull RecipientId recipientId) {
|
||||
repository.block(recipientId,
|
||||
() -> {
|
||||
loadRecipients();
|
||||
events.postValue(new Event(EventType.BLOCK_SUCCEEDED, Recipient.resolved(recipientId)));
|
||||
events.onNext(new Event(EventType.BLOCK_SUCCEEDED, Recipient.resolved(recipientId)));
|
||||
},
|
||||
() -> events.postValue(new Event(EventType.BLOCK_FAILED, Recipient.resolved(recipientId))));
|
||||
() -> events.onNext(new Event(EventType.BLOCK_FAILED, Recipient.resolved(recipientId))));
|
||||
}
|
||||
|
||||
void createAndBlock(@NonNull String number) {
|
||||
repository.createAndBlock(number, () -> {
|
||||
loadRecipients();
|
||||
events.postValue(new Event(EventType.BLOCK_SUCCEEDED, number));
|
||||
events.onNext(new Event(EventType.BLOCK_SUCCEEDED, number));
|
||||
});
|
||||
}
|
||||
|
||||
void unblock(@NonNull RecipientId recipientId) {
|
||||
repository.unblock(recipientId, () -> {
|
||||
loadRecipients();
|
||||
events.postValue(new Event(EventType.UNBLOCK_SUCCEEDED, Recipient.resolved(recipientId)));
|
||||
events.onNext(new Event(EventType.UNBLOCK_SUCCEEDED, Recipient.resolved(recipientId)));
|
||||
});
|
||||
}
|
||||
|
||||
private void loadRecipients() {
|
||||
repository.getBlocked(recipients::postValue);
|
||||
repository.getBlocked(recipients::onNext);
|
||||
}
|
||||
|
||||
enum EventType {
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -337,7 +338,7 @@ public final class AudioView extends FrameLayout {
|
||||
super.setClickable(clickable);
|
||||
this.playPauseButton.setClickable(clickable);
|
||||
this.seekBar.setClickable(clickable);
|
||||
this.seekBar.setOnTouchListener(clickable ? null : new TouchIgnoringListener());
|
||||
this.seekBar.setOnTouchListener(clickable ? new LongTapAwareTouchListener() : new TouchIgnoringListener());
|
||||
this.downloadButton.setClickable(clickable);
|
||||
}
|
||||
|
||||
@@ -505,6 +506,20 @@ public final class AudioView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
private class LongTapAwareTouchListener implements OnTouchListener {
|
||||
private final GestureDetector gestureDetector = new GestureDetector(AudioView.this.getContext(), new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
performLongClick();
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
return gestureDetector.onTouchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TouchIgnoringListener implements OnTouchListener {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
|
||||
@@ -489,6 +489,10 @@ public class InputPanel extends LinearLayout
|
||||
mediaKeyboard.setToMedia();
|
||||
}
|
||||
|
||||
public void setToIme() {
|
||||
mediaKeyboard.setToIme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
composeText.dispatchKeyEvent(keyEvent);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
|
||||
/**
|
||||
* Wraps a normal progress dialog for showing blocking in-progress UI.
|
||||
*/
|
||||
class SignalProgressDialog private constructor(val progressDialog: ProgressDialog) {
|
||||
|
||||
val isShowing: Boolean
|
||||
get() = progressDialog.isShowing
|
||||
|
||||
fun hide() {
|
||||
progressDialog.hide()
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
progressDialog.dismiss()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun show(
|
||||
context: Context,
|
||||
title: CharSequence? = null,
|
||||
message: CharSequence? = null,
|
||||
indeterminate: Boolean = false,
|
||||
cancelable: Boolean = false,
|
||||
cancelListener: DialogInterface.OnCancelListener? = null
|
||||
): SignalProgressDialog {
|
||||
return SignalProgressDialog(ProgressDialog.show(context, title, message, indeterminate, cancelable, cancelListener))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,12 @@ import androidx.annotation.Px;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.material.shape.MaterialShapeDrawable;
|
||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
/**
|
||||
* Class for creating simple tooltips to show throughout the app. Utilizes a popup window so you
|
||||
@@ -42,6 +44,8 @@ public class TooltipPopup extends PopupWindow {
|
||||
private final int position;
|
||||
private final int startMargin;
|
||||
|
||||
private final MaterialShapeDrawable shapeableBubbleBackground = new MaterialShapeDrawable();
|
||||
|
||||
public static Builder forTarget(@NonNull View anchor) {
|
||||
return new Builder(anchor);
|
||||
}
|
||||
@@ -85,9 +89,11 @@ public class TooltipPopup extends PopupWindow {
|
||||
if (backgroundTint == 0) {
|
||||
bubble.getBackground().setColorFilter(ContextCompat.getColor(anchor.getContext(), R.color.tooltip_default_color), PorterDuff.Mode.MULTIPLY);
|
||||
arrow.setColorFilter(ContextCompat.getColor(anchor.getContext(), R.color.tooltip_default_color), PorterDuff.Mode.SRC_IN);
|
||||
shapeableBubbleBackground.setTint(ContextCompat.getColor(anchor.getContext(), R.color.tooltip_default_color));
|
||||
} else {
|
||||
bubble.getBackground().setColorFilter(backgroundTint, PorterDuff.Mode.MULTIPLY);
|
||||
arrow.setColorFilter(backgroundTint, PorterDuff.Mode.SRC_IN);
|
||||
shapeableBubbleBackground.setTint(backgroundTint);
|
||||
}
|
||||
|
||||
if (iconGlideModel != null) {
|
||||
@@ -161,6 +167,26 @@ public class TooltipPopup extends PopupWindow {
|
||||
xoffset -= startMargin;
|
||||
}
|
||||
|
||||
View bubble = getContentView().findViewById(R.id.tooltip_bubble);
|
||||
ShapeAppearanceModel.Builder shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
.setAllCornerSizes(DimensionUnit.DP.toPixels(18));
|
||||
|
||||
// If the arrow is within the last 20dp of the right hand side, use RIGHT and set corner to 9dp
|
||||
onLayout(() -> {
|
||||
if (arrow.getX() > getContentView().getWidth() / 2f) {
|
||||
arrow.setImageResource(R.drawable.ic_tooltip_arrow_up_right);
|
||||
}
|
||||
|
||||
float arrowEnd = arrow.getX() + arrow.getRight();
|
||||
if (arrowEnd > getContentView().getRight() - DimensionUnit.DP.toPixels(20)) {
|
||||
shapeableBubbleBackground.setShapeAppearanceModel(shapeAppearanceModel.setTopRightCornerSize(DimensionUnit.DP.toPixels(9f)).build());
|
||||
bubble.setBackground(shapeableBubbleBackground);
|
||||
} else if (arrowEnd < DimensionUnit.DP.toPixels(20)) {
|
||||
shapeableBubbleBackground.setShapeAppearanceModel(shapeAppearanceModel.setTopLeftCornerSize(DimensionUnit.DP.toPixels(9f)).build());
|
||||
bubble.setBackground(shapeableBubbleBackground);
|
||||
}
|
||||
});
|
||||
|
||||
showAsDropDown(anchor, xoffset, yoffset);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,7 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.util.AttributeSet;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -17,15 +12,11 @@ import androidx.appcompat.widget.AppCompatEditText;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.EditTextExtensionsKt;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
@@ -104,44 +95,4 @@ public class EmojiEditText extends AppCompatEditText {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTextContextMenuItem(int id) {
|
||||
if (id == android.R.id.paste) {
|
||||
ClipboardManager clipboardManager = ServiceUtil.getClipboardManager(getContext());
|
||||
ClipData originalClipData = clipboardManager.getPrimaryClip();
|
||||
CharSequence pendingPaste = getTextFromClipData(originalClipData);
|
||||
|
||||
if (pendingPaste == null) {
|
||||
return super.onTextContextMenuItem(id);
|
||||
}
|
||||
|
||||
CharSequence sanitizedText = (pendingPaste instanceof Spanned) ? clearFormattingFromText(pendingPaste)
|
||||
: pendingPaste;
|
||||
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("signal_sanitized", sanitizedText));
|
||||
boolean performedAction = super.onTextContextMenuItem(id);
|
||||
|
||||
clipboardManager.setPrimaryClip(originalClipData);
|
||||
return performedAction;
|
||||
}
|
||||
return super.onTextContextMenuItem(id);
|
||||
}
|
||||
|
||||
private CharSequence clearFormattingFromText(CharSequence text) {
|
||||
List<Mention> mentions = MentionAnnotation.getMentionsFromAnnotations(text);
|
||||
Spannable withoutFormatting = new SpannableString(text.toString());
|
||||
|
||||
MentionAnnotation.setMentionAnnotations(withoutFormatting, mentions);
|
||||
|
||||
return withoutFormatting;
|
||||
}
|
||||
|
||||
private @Nullable CharSequence getTextFromClipData(ClipData data) {
|
||||
if (data != null && data.getItemCount() > 0) {
|
||||
return data.getItemAt(0).coerceToText(getContext());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
|
||||
import org.thoughtcrime.securesms.DatabaseMigrationActivity;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
|
||||
|
||||
public class SystemSmsImportReminder extends Reminder {
|
||||
|
||||
public SystemSmsImportReminder(final Context context) {
|
||||
super(context.getString(R.string.reminder_header_sms_import_title),
|
||||
context.getString(R.string.reminder_header_sms_import_text));
|
||||
|
||||
final OnClickListener okListener = v -> {
|
||||
Intent intent = new Intent(context, ApplicationMigrationService.class);
|
||||
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
|
||||
context.startService(intent);
|
||||
|
||||
// TODO [greyson] Navigation
|
||||
Intent nextIntent = MainActivity.clearTop(context);
|
||||
Intent activityIntent = new Intent(context, DatabaseMigrationActivity.class);
|
||||
activityIntent.putExtra("next_intent", nextIntent);
|
||||
context.startActivity(activityIntent);
|
||||
};
|
||||
final OnClickListener cancelListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ApplicationMigrationService.setDatabaseImported(context);
|
||||
}
|
||||
};
|
||||
setOkListener(okListener);
|
||||
setDismissListener(cancelListener);
|
||||
}
|
||||
|
||||
public static boolean isEligible(Context context) {
|
||||
return !ApplicationMigrationService.isDatabaseImported(context);
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,7 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
|
||||
@@ -11,6 +11,9 @@ import org.signal.donations.StripeDeclineCode
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getBoostBadges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getSubscriptionLevels
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -29,28 +32,28 @@ class InternalDonorErrorConfigurationViewModel : ViewModel() {
|
||||
val giftBadges: Single<List<Badge>> = Single
|
||||
.fromCallable {
|
||||
ApplicationDependencies.getDonationsService()
|
||||
.getGiftBadges(Locale.getDefault())
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { results -> results.values.map { Badges.fromServiceBadge(it) } }
|
||||
.map { it.getGiftBadges() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
val boostBadges: Single<List<Badge>> = Single
|
||||
.fromCallable {
|
||||
ApplicationDependencies.getDonationsService()
|
||||
.getBoostBadge(Locale.getDefault())
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { listOf(Badges.fromServiceBadge(it)) }
|
||||
.map { it.getBoostBadges() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
val subscriptionBadges: Single<List<Badge>> = Single
|
||||
.fromCallable {
|
||||
ApplicationDependencies.getDonationsService()
|
||||
.getSubscriptionLevels(Locale.getDefault())
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { levels -> levels.levels.values.map { Badges.fromServiceBadge(it.badge) } }
|
||||
.map { config -> config.getSubscriptionLevels().values.map { Badges.fromServiceBadge(it.badge) } }
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
disposables += Single.zip(giftBadges, boostBadges, subscriptionBadges) { g, b, s ->
|
||||
|
||||
@@ -104,7 +104,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
|
||||
summary = DSLSettingsText.from(R.string.preferences__change_sound_and_vibration),
|
||||
isEnabled = state.messageNotificationsState.notificationsEnabled,
|
||||
onClick = {
|
||||
NotificationChannels.getInstance().openChannelSettings(NotificationChannels.getInstance().messagesChannel, null)
|
||||
NotificationChannels.getInstance().openChannelSettings(requireActivity(), NotificationChannels.getInstance().messagesChannel, null)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -344,7 +344,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
|
||||
if (!ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure) {
|
||||
showGoToPhoneSettings()
|
||||
} else if (state.paymentLock) {
|
||||
biometricAuth.authenticate(requireContext(), true) { biometricDeviceLockLauncher?.launch(getString(R.string.BiometricDeviceAuthentication__signal)) }
|
||||
biometricAuth.authenticate(requireContext(), true) { biometricDeviceLockLauncher.launch(getString(R.string.BiometricDeviceAuthentication__signal)) }
|
||||
} else {
|
||||
viewModel.togglePaymentLock(true)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.privacy.advanced
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -18,6 +17,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.SignalProgressDialog
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
@@ -48,7 +48,7 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences
|
||||
}
|
||||
}
|
||||
|
||||
var progressDialog: ProgressDialog? = null
|
||||
var progressDialog: SignalProgressDialog? = null
|
||||
|
||||
val statusIcon: CharSequence by lazy {
|
||||
val unidentifiedDeliveryIcon = requireNotNull(
|
||||
@@ -85,7 +85,7 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
if (it.showProgressSpinner) {
|
||||
if (progressDialog?.isShowing == false) {
|
||||
progressDialog = ProgressDialog.show(requireContext(), null, null, true)
|
||||
progressDialog = SignalProgressDialog.show(requireContext(), null, null, true)
|
||||
}
|
||||
} else {
|
||||
progressDialog?.hide()
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
@@ -39,7 +39,7 @@ class ExpireTimerSettingsRepository(val context: Context) {
|
||||
}
|
||||
} else {
|
||||
SignalDatabase.recipients.setExpireMessages(recipientId, newExpirationTime)
|
||||
val outgoingMessage = OutgoingExpirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L)
|
||||
val outgoingMessage = OutgoingMediaMessage.expirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L)
|
||||
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), false, null, null)
|
||||
consumer.invoke(Result.success(newExpirationTime))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.money.PlatformCurrencyUtil
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.whispersystems.signalservice.internal.push.DonationsConfiguration
|
||||
import org.whispersystems.signalservice.internal.push.DonationsConfiguration.BOOST_LEVEL
|
||||
import org.whispersystems.signalservice.internal.push.DonationsConfiguration.GIFT_LEVEL
|
||||
import org.whispersystems.signalservice.internal.push.DonationsConfiguration.LevelConfiguration
|
||||
import org.whispersystems.signalservice.internal.push.DonationsConfiguration.SUBSCRIPTION_LEVELS
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
private const val CARD = "CARD"
|
||||
private const val PAYPAL = "PAYPAL"
|
||||
|
||||
/**
|
||||
* Transforms the DonationsConfiguration into a Set<FiatMoney> which has been properly filtered
|
||||
* for available currencies on the platform and based off user device availability.
|
||||
*
|
||||
* CARD - Google Pay & Credit Card
|
||||
* PAYPAL - PayPal
|
||||
*
|
||||
* @param level The subscription level to get amounts for
|
||||
* @param paymentMethodAvailability Predicate object which checks whether different payment methods are availble.
|
||||
*/
|
||||
fun DonationsConfiguration.getSubscriptionAmounts(
|
||||
level: Int,
|
||||
paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability
|
||||
): Set<FiatMoney> {
|
||||
require(SUBSCRIPTION_LEVELS.contains(level))
|
||||
|
||||
return getFilteredCurrencies(paymentMethodAvailability).map { (code, config) ->
|
||||
val amount: BigDecimal = config.subscription[level]!!
|
||||
FiatMoney(amount, Currency.getInstance(code.uppercase()))
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently, we only support a single gift badge at level GIFT_LEVEL
|
||||
*/
|
||||
fun DonationsConfiguration.getGiftBadges(): List<Badge> {
|
||||
val configuration = levels[GIFT_LEVEL]
|
||||
return listOfNotNull(configuration?.badge?.let { Badges.fromServiceBadge(it) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently, we only support a single gift badge amount per currency
|
||||
*/
|
||||
fun DonationsConfiguration.getGiftBadgeAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, FiatMoney> {
|
||||
return getFilteredCurrencies(paymentMethodAvailability).filter {
|
||||
it.value.oneTime[GIFT_LEVEL]?.isNotEmpty() == true
|
||||
}.mapKeys {
|
||||
Currency.getInstance(it.key.uppercase())
|
||||
}.mapValues {
|
||||
FiatMoney(it.value.oneTime[GIFT_LEVEL]!!.first(), it.key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently, we only support a single boost badge at level BOOST_LEVEL
|
||||
*/
|
||||
fun DonationsConfiguration.getBoostBadges(): List<Badge> {
|
||||
val configuration = levels[BOOST_LEVEL]
|
||||
return listOfNotNull(configuration?.badge?.let { Badges.fromServiceBadge(it) })
|
||||
}
|
||||
|
||||
fun DonationsConfiguration.getBoostAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, List<FiatMoney>> {
|
||||
return getFilteredCurrencies(paymentMethodAvailability).filter {
|
||||
it.value.oneTime[BOOST_LEVEL]?.isNotEmpty() == true
|
||||
}.mapKeys {
|
||||
Currency.getInstance(it.key.uppercase())
|
||||
}.mapValues { (currency, config) ->
|
||||
config.oneTime[BOOST_LEVEL]!!.map { FiatMoney(it, currency) }
|
||||
}
|
||||
}
|
||||
|
||||
fun DonationsConfiguration.getBadge(level: Int): Badge {
|
||||
require(level == GIFT_LEVEL || level == BOOST_LEVEL || SUBSCRIPTION_LEVELS.contains(level))
|
||||
return Badges.fromServiceBadge(levels[level]!!.badge)
|
||||
}
|
||||
|
||||
fun DonationsConfiguration.getSubscriptionLevels(): Map<Int, LevelConfiguration> {
|
||||
return levels.filterKeys { SUBSCRIPTION_LEVELS.contains(it) }.toSortedMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map describing the minimum donation amounts per currency.
|
||||
* This returns only the currencies available to the user.
|
||||
*/
|
||||
fun DonationsConfiguration.getMinimumDonationAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, FiatMoney> {
|
||||
return getFilteredCurrencies(paymentMethodAvailability)
|
||||
.mapKeys { Currency.getInstance(it.key.uppercase()) }
|
||||
.mapValues { FiatMoney(it.value.minimum, it.key) }
|
||||
}
|
||||
|
||||
private fun DonationsConfiguration.getFilteredCurrencies(paymentMethodAvailability: PaymentMethodAvailability): Map<String, DonationsConfiguration.CurrencyConfiguration> {
|
||||
val userPaymentMethods = paymentMethodAvailability.toSet()
|
||||
val availableCurrencyCodes = PlatformCurrencyUtil.getAvailableCurrencyCodes()
|
||||
return currencies.filter { (code, config) ->
|
||||
val areAllMethodsAvailable = config.supportedPaymentMethods.containsAll(userPaymentMethods)
|
||||
availableCurrencyCodes.contains(code.uppercase()) && areAllMethodsAvailable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface is available to ease unit testing of the extension methods in
|
||||
* this file. In all normal situations, you can just allow the methods to use the
|
||||
* default value.
|
||||
*/
|
||||
interface PaymentMethodAvailability {
|
||||
fun isPayPalAvailable(): Boolean
|
||||
fun isGooglePayOrCreditCardAvailable(): Boolean
|
||||
|
||||
fun toSet(): Set<String> {
|
||||
val set = mutableSetOf<String>()
|
||||
if (isPayPalAvailable()) {
|
||||
set.add(PAYPAL)
|
||||
}
|
||||
|
||||
if (isGooglePayOrCreditCardAvailable()) {
|
||||
set.add(CARD)
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
}
|
||||
|
||||
private object DefaultPaymentMethodAvailability : PaymentMethodAvailability {
|
||||
override fun isPayPalAvailable(): Boolean = InAppDonations.isPayPalAvailable()
|
||||
override fun isGooglePayOrCreditCardAvailable(): Boolean = InAppDonations.isCreditCardAvailable() || InAppDonations.isGooglePayAvailable()
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.LocaleFeatureFlags
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||
|
||||
/**
|
||||
* Helper object to determine in-app donations availability.
|
||||
@@ -21,6 +22,22 @@ object InAppDonations {
|
||||
return isCreditCardAvailable() || isPayPalAvailable() || isGooglePayAvailable()
|
||||
}
|
||||
|
||||
fun isPaymentSourceAvailable(paymentSourceType: PaymentSourceType, donateToSignalType: DonateToSignalType): Boolean {
|
||||
return when (paymentSourceType) {
|
||||
PaymentSourceType.PayPal -> isPayPalAvailableForDonateToSignalType(donateToSignalType)
|
||||
PaymentSourceType.Stripe.CreditCard -> isCreditCardAvailable()
|
||||
PaymentSourceType.Stripe.GooglePay -> isGooglePayAvailable()
|
||||
PaymentSourceType.Unknown -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPayPalAvailableForDonateToSignalType(donateToSignalType: DonateToSignalType): Boolean {
|
||||
return when (donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME, DonateToSignalType.GIFT -> FeatureFlags.paypalOneTimeDonations()
|
||||
DonateToSignalType.MONTHLY -> FeatureFlags.paypalRecurringDonations()
|
||||
} && !LocaleFeatureFlags.isPayPalDisabled()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is in a region that supports credit cards, based off local phone number.
|
||||
*/
|
||||
@@ -32,21 +49,13 @@ object InAppDonations {
|
||||
* Whether the user is in a region that supports PayPal, based off local phone number.
|
||||
*/
|
||||
fun isPayPalAvailable(): Boolean {
|
||||
return FeatureFlags.paypalDonations() && !LocaleFeatureFlags.isPayPalDisabled()
|
||||
return (FeatureFlags.paypalOneTimeDonations() || FeatureFlags.paypalRecurringDonations()) && !LocaleFeatureFlags.isPayPalDisabled()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is in a region that supports GooglePay, based off local phone number.
|
||||
* Whether the user is using a device that supports GooglePay, based off Wallet API and phone number.
|
||||
*/
|
||||
private fun isGooglePayAvailable(): Boolean {
|
||||
return isPlayServicesAvailable() && !LocaleFeatureFlags.isGooglePayDisabled()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether Play Services is available. This will *not* tell you whether a user has Google Pay set up, but is
|
||||
* enough information to determine whether we can display Google Pay as an option.
|
||||
*/
|
||||
private fun isPlayServicesAvailable(): Boolean {
|
||||
return PlayServicesUtil.getPlayServicesStatus(ApplicationDependencies.getApplication()) == PlayServicesUtil.PlayServicesStatus.SUCCESS
|
||||
fun isGooglePayAvailable(): Boolean {
|
||||
return SignalStore.donationsValues().isGooglePayReady && !LocaleFeatureFlags.isGooglePayDisabled()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
@@ -20,15 +19,12 @@ import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionLevels
|
||||
import org.whispersystems.signalservice.internal.EmptyResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -52,29 +48,23 @@ class MonthlyDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubscriptions(): Single<List<Subscription>> = Single
|
||||
.fromCallable { donationsService.getSubscriptionLevels(Locale.getDefault()) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(ServiceResponse<SubscriptionLevels>::flattenResult)
|
||||
.map { subscriptionLevels ->
|
||||
subscriptionLevels.levels.map { (code, level) ->
|
||||
Subscription(
|
||||
id = code,
|
||||
name = level.name,
|
||||
badge = Badges.fromServiceBadge(level.badge),
|
||||
prices = level.currencies.filter {
|
||||
PlatformCurrencyUtil
|
||||
.getAvailableCurrencyCodes()
|
||||
.contains(it.key)
|
||||
}.map { (currencyCode, price) ->
|
||||
FiatMoney(price, Currency.getInstance(currencyCode))
|
||||
}.toSet(),
|
||||
level = code.toInt()
|
||||
)
|
||||
}.sortedBy {
|
||||
it.level
|
||||
fun getSubscriptions(): Single<List<Subscription>> {
|
||||
return Single
|
||||
.fromCallable { donationsService.getDonationsConfiguration(Locale.getDefault()) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { config ->
|
||||
config.getSubscriptionLevels().map { (level, levelConfig) ->
|
||||
Subscription(
|
||||
id = level.toString(),
|
||||
level = level,
|
||||
name = levelConfig.name,
|
||||
badge = Badges.fromServiceBadge(levelConfig.badge),
|
||||
prices = config.getSubscriptionAmounts(level)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun syncAccountRecord(): Completable {
|
||||
return Completable.fromAction {
|
||||
|
||||
@@ -6,7 +6,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
@@ -18,12 +17,8 @@ import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.services.DonationsService
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.DonationProcessor
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.CountDownLatch
|
||||
@@ -34,7 +29,7 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
companion object {
|
||||
private val TAG = Log.tag(OneTimeDonationRepository::class.java)
|
||||
|
||||
fun <T> handleCreatePaymentIntentError(throwable: Throwable, badgeRecipient: RecipientId, paymentSourceType: PaymentSourceType): Single<T> {
|
||||
fun <T : Any> handleCreatePaymentIntentError(throwable: Throwable, badgeRecipient: RecipientId, paymentSourceType: PaymentSourceType): Single<T> {
|
||||
return if (throwable is DonationError) {
|
||||
Single.error(throwable)
|
||||
} else {
|
||||
@@ -46,14 +41,15 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
}
|
||||
|
||||
fun getBoosts(): Single<Map<Currency, List<Boost>>> {
|
||||
return Single.fromCallable { donationsService.boostAmounts }
|
||||
return Single.fromCallable { donationsService.getDonationsConfiguration(Locale.getDefault()) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(ServiceResponse<Map<String, List<BigDecimal>>>::flattenResult)
|
||||
.map { result ->
|
||||
result
|
||||
.filter { PlatformCurrencyUtil.getAvailableCurrencyCodes().contains(it.key) }
|
||||
.mapKeys { (code, _) -> Currency.getInstance(code) }
|
||||
.mapValues { (currency, prices) -> prices.map { Boost(FiatMoney(it, currency)) } }
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { config ->
|
||||
config.getBoostAmounts().mapValues { (_, value) ->
|
||||
value.map {
|
||||
Boost(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,11 +57,18 @@ class OneTimeDonationRepository(private val donationsService: DonationsService)
|
||||
return Single
|
||||
.fromCallable {
|
||||
ApplicationDependencies.getDonationsService()
|
||||
.getBoostBadge(Locale.getDefault())
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(ServiceResponse<SignalServiceProfile.Badge>::flattenResult)
|
||||
.map(Badges::fromServiceBadge)
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { it.getBoostBadges().first() }
|
||||
}
|
||||
|
||||
fun getMinimumDonationAmounts(): Single<Map<Currency, FiatMoney>> {
|
||||
return Single.fromCallable { donationsService.getDonationsConfiguration(Locale.getDefault()) }
|
||||
.flatMap { it.flattenResult() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { it.getMinimumDonationAmounts() }
|
||||
}
|
||||
|
||||
fun waitForOneTimeRedemption(
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal.PayPalConfirmationResult
|
||||
@@ -24,6 +25,8 @@ class PayPalRepository(private val donationsService: DonationsService) {
|
||||
const val ONE_TIME_RETURN_URL = "https://signaldonations.org/return/onetime"
|
||||
const val MONTHLY_RETURN_URL = "https://signaldonations.org/return/monthly"
|
||||
const val CANCEL_URL = "https://signaldonations.org/cancel"
|
||||
|
||||
private val TAG = Log.tag(PayPalRepository::class.java)
|
||||
}
|
||||
|
||||
fun createOneTimePaymentIntent(
|
||||
@@ -53,6 +56,7 @@ class PayPalRepository(private val donationsService: DonationsService) {
|
||||
paypalConfirmationResult: PayPalConfirmationResult
|
||||
): Single<PayPalConfirmPaymentIntentResponse> {
|
||||
return Single.fromCallable {
|
||||
Log.d(TAG, "Confirming one-time payment intent...", true)
|
||||
donationsService
|
||||
.confirmPayPalOneTimePaymentIntent(
|
||||
amount.currency.currencyCode,
|
||||
@@ -78,11 +82,14 @@ class PayPalRepository(private val donationsService: DonationsService) {
|
||||
|
||||
fun setDefaultPaymentMethod(paymentMethodId: String): Completable {
|
||||
return Single.fromCallable {
|
||||
Log.d(TAG, "Setting default payment method...", true)
|
||||
donationsService.setDefaultPayPalPaymentMethod(
|
||||
SignalStore.donationsValues().requireSubscriber().subscriberId,
|
||||
paymentMethodId
|
||||
)
|
||||
}.flatMap { it.flattenResult() }.ignoreElement().andThen {
|
||||
}.flatMap { it.flattenResult() }.ignoreElement().doOnComplete {
|
||||
Log.d(TAG, "Set default payment method.", true)
|
||||
Log.d(TAG, "Storing the subscription payment source type locally.", true)
|
||||
SignalStore.donationsValues().setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.text.Spanned
|
||||
import android.text.TextWatcher
|
||||
import android.text.method.DigitsKeyListener
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.animation.doOnEnd
|
||||
@@ -26,6 +27,7 @@ import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.lang.Integer.min
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
@@ -102,7 +104,9 @@ data class Boost(
|
||||
val currency: Currency,
|
||||
override val isEnabled: Boolean,
|
||||
val onBoostClick: (View, Boost) -> Unit,
|
||||
val minimumAmount: FiatMoney,
|
||||
val isCustomAmountFocused: Boolean,
|
||||
val isCustomAmountTooSmall: Boolean,
|
||||
val onCustomAmountChanged: (String) -> Unit,
|
||||
val onCustomAmountFocusChanged: (Boolean) -> Unit,
|
||||
) : PreferenceModel<SelectionModel>(isEnabled = isEnabled) {
|
||||
@@ -113,7 +117,10 @@ data class Boost(
|
||||
newItem.boosts == boosts &&
|
||||
newItem.selectedBoost == selectedBoost &&
|
||||
newItem.currency == currency &&
|
||||
newItem.isCustomAmountFocused == isCustomAmountFocused
|
||||
newItem.isCustomAmountFocused == isCustomAmountFocused &&
|
||||
newItem.isCustomAmountTooSmall == isCustomAmountTooSmall &&
|
||||
newItem.minimumAmount.amount == minimumAmount.amount &&
|
||||
newItem.minimumAmount.currency == minimumAmount.currency
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +133,7 @@ data class Boost(
|
||||
private val boost5: MaterialButton = itemView.findViewById(R.id.boost_5)
|
||||
private val boost6: MaterialButton = itemView.findViewById(R.id.boost_6)
|
||||
private val custom: AppCompatEditText = itemView.findViewById(R.id.boost_custom)
|
||||
private val error: TextView = itemView.findViewById(R.id.boost_custom_too_small)
|
||||
|
||||
private val boostButtons: List<MaterialButton>
|
||||
get() {
|
||||
@@ -145,6 +153,16 @@ data class Boost(
|
||||
override fun bind(model: SelectionModel) {
|
||||
itemView.isEnabled = model.isEnabled
|
||||
|
||||
error.text = context.getString(
|
||||
R.string.Boost__the_minimum_amount_you_can_donate_is_s,
|
||||
FiatMoneyUtil.format(
|
||||
context.resources, model.minimumAmount,
|
||||
FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()
|
||||
)
|
||||
)
|
||||
|
||||
error.visible = model.isCustomAmountTooSmall
|
||||
|
||||
model.boosts.zip(boostButtons).forEach { (boost, button) ->
|
||||
val isSelected = boost == model.selectedBoost && !model.isCustomAmountFocused
|
||||
button.isSelected = isSelected
|
||||
|
||||
@@ -23,6 +23,7 @@ class DonateToSignalActivity : FragmentWrapperActivity(), DonationPaymentCompone
|
||||
return NavHostFragment.create(R.navigation.donate_to_signal, DonateToSignalFragmentArgs.Builder(DonateToSignalType.ONE_TIME).build().toBundle())
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -16,7 +14,6 @@ import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.core.util.dp
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
@@ -30,9 +27,6 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorParams
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
@@ -80,8 +74,6 @@ class DonateToSignalFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private var errorDialog: DialogInterface? = null
|
||||
|
||||
private val args: DonateToSignalFragmentArgs by navArgs()
|
||||
private val viewModel: DonateToSignalViewModel by viewModels(factoryProducer = {
|
||||
DonateToSignalViewModel.Factory(args.startType)
|
||||
@@ -114,7 +106,7 @@ class DonateToSignalFragment :
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: MappingAdapter) {
|
||||
donationCheckoutDelegate = DonationCheckoutDelegate(this, this)
|
||||
donationCheckoutDelegate = DonationCheckoutDelegate(this, this, DonationErrorSource.BOOST, DonationErrorSource.SUBSCRIPTION)
|
||||
|
||||
val recyclerView = this.recyclerView!!
|
||||
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||
@@ -139,19 +131,6 @@ class DonateToSignalFragment :
|
||||
DonationPillToggle.register(adapter)
|
||||
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
|
||||
disposables += DonationError.getErrorsForSource(DonationErrorSource.BOOST)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { error ->
|
||||
showErrorDialog(error)
|
||||
}
|
||||
|
||||
disposables += DonationError.getErrorsForSource(DonationErrorSource.SUBSCRIPTION)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { error ->
|
||||
showErrorDialog(error)
|
||||
}
|
||||
|
||||
disposables += viewModel.actions.subscribe { action ->
|
||||
when (action) {
|
||||
is DonateToSignalAction.DisplayCurrencySelectionDialog -> {
|
||||
@@ -263,6 +242,7 @@ class DonateToSignalFragment :
|
||||
when (state.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> displayOneTimeSelection(state.areFieldsEnabled, state.oneTimeDonationState)
|
||||
DonateToSignalType.MONTHLY -> displayMonthlySelection(state.areFieldsEnabled, state.monthlyDonationState)
|
||||
DonateToSignalType.GIFT -> error("This fragment does not support gifts.")
|
||||
}
|
||||
|
||||
space(20.dp)
|
||||
@@ -331,6 +311,8 @@ class DonateToSignalFragment :
|
||||
selectedBoost = state.selectedBoost,
|
||||
currency = state.customAmount.currency,
|
||||
isCustomAmountFocused = state.isCustomAmountFocused,
|
||||
isCustomAmountTooSmall = state.shouldDisplayCustomAmountTooSmallError,
|
||||
minimumAmount = state.minimumDonationAmountOfSelectedCurrency,
|
||||
isEnabled = areFieldsEnabled,
|
||||
onBoostClick = { view, boost ->
|
||||
startAnimationAboveSelectedBoost(view)
|
||||
@@ -389,36 +371,6 @@ class DonateToSignalFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorDialog(throwable: Throwable) {
|
||||
if (errorDialog != null) {
|
||||
Log.d(TAG, "Already displaying an error dialog. Skipping.", throwable, true)
|
||||
} else {
|
||||
Log.d(TAG, "Displaying donation error dialog.", true)
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
requireContext(), throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
var tryCCAgain = false
|
||||
|
||||
override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction<Unit>? {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__try,
|
||||
action = {
|
||||
tryCCAgain = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogDismissed() {
|
||||
errorDialog = null
|
||||
if (!tryCCAgain) {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAnimationAboveSelectedBoost(view: View) {
|
||||
val animationView = getAnimationContainer(view)
|
||||
val viewProjection = Projection.relativeToViewRoot(view, null)
|
||||
@@ -472,4 +424,8 @@ class DonateToSignalFragment :
|
||||
override fun onProcessorActionProcessed() {
|
||||
viewModel.refreshActiveSubscription()
|
||||
}
|
||||
|
||||
override fun onUserCancelledPaymentFlow() {
|
||||
findNavController().popBackStack(R.id.donateToSignalFragment, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +81,15 @@ data class DonateToSignalState(
|
||||
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, selectedCurrency),
|
||||
val isCustomAmountFocused: Boolean = false,
|
||||
val donationStage: DonationStage = DonationStage.INIT,
|
||||
val selectableCurrencyCodes: List<String> = emptyList()
|
||||
val selectableCurrencyCodes: List<String> = emptyList(),
|
||||
private val minimumDonationAmounts: Map<Currency, FiatMoney> = emptyMap()
|
||||
) {
|
||||
val isSelectionValid: Boolean = if (isCustomAmountFocused) customAmount.amount > BigDecimal.ZERO else selectedBoost != null
|
||||
val minimumDonationAmountOfSelectedCurrency: FiatMoney = minimumDonationAmounts[selectedCurrency] ?: FiatMoney(BigDecimal.ZERO, selectedCurrency)
|
||||
private val isCustomAmountTooSmall: Boolean = if (isCustomAmountFocused) customAmount.amount < minimumDonationAmountOfSelectedCurrency.amount else false
|
||||
private val isCustomAmountZero: Boolean = customAmount.amount == BigDecimal.ZERO
|
||||
|
||||
val isSelectionValid: Boolean = if (isCustomAmountFocused) !isCustomAmountTooSmall else selectedBoost != null
|
||||
val shouldDisplayCustomAmountTooSmallError: Boolean = isCustomAmountTooSmall && !isCustomAmountZero
|
||||
}
|
||||
|
||||
data class MonthlyDonationState(
|
||||
|
||||
@@ -2,10 +2,19 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
|
||||
@Parcelize
|
||||
enum class DonateToSignalType(val requestCode: Short) : Parcelable {
|
||||
ONE_TIME(16141),
|
||||
MONTHLY(16142),
|
||||
GIFT(16143)
|
||||
GIFT(16143);
|
||||
|
||||
fun toErrorSource(): DonationErrorSource {
|
||||
return when (this) {
|
||||
ONE_TIME -> DonationErrorSource.BOOST
|
||||
MONTHLY -> DonationErrorSource.SUBSCRIPTION
|
||||
GIFT -> DonationErrorSource.GIFT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.money.PlatformCurrencyUtil
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.MonthlyDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.OneTimeDonationRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
@@ -25,7 +26,6 @@ import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
@@ -214,6 +214,15 @@ class DonateToSignalViewModel(
|
||||
}
|
||||
)
|
||||
|
||||
oneTimeDonationDisposables += oneTimeDonationRepository.getMinimumDonationAmounts().subscribeBy(
|
||||
onSuccess = { amountMap ->
|
||||
store.update { it.copy(oneTimeDonationState = it.oneTimeDonationState.copy(minimumDonationAmounts = amountMap)) }
|
||||
},
|
||||
onError = {
|
||||
Log.w(TAG, "Could not load minimum custom donation amounts.", it)
|
||||
}
|
||||
)
|
||||
|
||||
val boosts: Observable<Map<Currency, List<Boost>>> = oneTimeDonationRepository.getBoosts().toObservable()
|
||||
val oneTimeCurrency: Observable<Currency> = SignalStore.donationsValues().observableOneTimeCurrency
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -10,6 +12,8 @@ import androidx.navigation.navGraphViewModels
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
@@ -18,7 +22,6 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.card.CreditCardResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayRequest
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet
|
||||
@@ -26,6 +29,8 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.donate.pa
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorParams
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
@@ -36,7 +41,9 @@ import java.util.Currency
|
||||
*/
|
||||
class DonationCheckoutDelegate(
|
||||
private val fragment: Fragment,
|
||||
private val callback: Callback
|
||||
private val callback: Callback,
|
||||
errorSource: DonationErrorSource,
|
||||
vararg additionalSources: DonationErrorSource
|
||||
) : DefaultLifecycleObserver {
|
||||
|
||||
companion object {
|
||||
@@ -57,6 +64,7 @@ class DonationCheckoutDelegate(
|
||||
|
||||
init {
|
||||
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||
ErrorHandler().attach(fragment, callback, errorSource, *additionalSources)
|
||||
}
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
@@ -75,8 +83,8 @@ class DonationCheckoutDelegate(
|
||||
}
|
||||
|
||||
fragment.setFragmentResultListener(CreditCardFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: CreditCardResult = bundle.getParcelable(CreditCardFragment.REQUEST_KEY)!!
|
||||
handleCreditCardResult(result)
|
||||
val result: DonationProcessorActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!!
|
||||
handleDonationProcessorActionResult(result)
|
||||
}
|
||||
|
||||
fragment.setFragmentResultListener(PayPalPaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
@@ -86,19 +94,17 @@ class DonationCheckoutDelegate(
|
||||
}
|
||||
|
||||
private fun handleGatewaySelectionResponse(gatewayResponse: GatewayResponse) {
|
||||
when (gatewayResponse.gateway) {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
|
||||
GatewayResponse.Gateway.PAYPAL -> launchPayPal(gatewayResponse)
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> launchCreditCard(gatewayResponse)
|
||||
if (InAppDonations.isPaymentSourceAvailable(gatewayResponse.gateway.toPaymentSourceType(), gatewayResponse.request.donateToSignalType)) {
|
||||
when (gatewayResponse.gateway) {
|
||||
GatewayResponse.Gateway.GOOGLE_PAY -> launchGooglePay(gatewayResponse)
|
||||
GatewayResponse.Gateway.PAYPAL -> launchPayPal(gatewayResponse)
|
||||
GatewayResponse.Gateway.CREDIT_CARD -> launchCreditCard(gatewayResponse)
|
||||
}
|
||||
} else {
|
||||
error("Unsupported combination! ${gatewayResponse.gateway} ${gatewayResponse.request.donateToSignalType}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreditCardResult(creditCardResult: CreditCardResult) {
|
||||
Log.d(TAG, "Received credit card information from fragment.")
|
||||
stripePaymentViewModel.provideCardData(creditCardResult.creditCardData)
|
||||
callback.navigateToStripePaymentInProgress(creditCardResult.gatewayRequest)
|
||||
}
|
||||
|
||||
private fun handleDonationProcessorActionResult(result: DonationProcessorActionResult) {
|
||||
when (result.status) {
|
||||
DonationProcessorActionResult.Status.SUCCESS -> handleSuccessfulDonationProcessorActionResult(result)
|
||||
@@ -131,11 +137,7 @@ class DonationCheckoutDelegate(
|
||||
}
|
||||
|
||||
private fun launchPayPal(gatewayResponse: GatewayResponse) {
|
||||
if (InAppDonations.isPayPalAvailable()) {
|
||||
callback.navigateToPayPalPaymentInProgress(gatewayResponse.request)
|
||||
} else {
|
||||
error("PayPal is not currently enabled.")
|
||||
}
|
||||
callback.navigateToPayPalPaymentInProgress(gatewayResponse.request)
|
||||
}
|
||||
|
||||
private fun launchGooglePay(gatewayResponse: GatewayResponse) {
|
||||
@@ -148,11 +150,7 @@ class DonationCheckoutDelegate(
|
||||
}
|
||||
|
||||
private fun launchCreditCard(gatewayResponse: GatewayResponse) {
|
||||
if (InAppDonations.isCreditCardAvailable()) {
|
||||
callback.navigateToCreditCardForm(gatewayResponse.request)
|
||||
} else {
|
||||
error("Credit cards are not currently enabled.")
|
||||
}
|
||||
callback.navigateToCreditCardForm(gatewayResponse.request)
|
||||
}
|
||||
|
||||
private fun registerGooglePayCallback() {
|
||||
@@ -198,7 +196,86 @@ class DonationCheckoutDelegate(
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
/**
|
||||
* Shared logic for handling checkout errors.
|
||||
*/
|
||||
class ErrorHandler : DefaultLifecycleObserver {
|
||||
|
||||
private var fragment: Fragment? = null
|
||||
private var errorDialog: DialogInterface? = null
|
||||
private var userCancelledFlowCallback: UserCancelledFlowCallback? = null
|
||||
|
||||
fun attach(fragment: Fragment, userCancelledFlowCallback: UserCancelledFlowCallback?, errorSource: DonationErrorSource, vararg additionalSources: DonationErrorSource) {
|
||||
this.fragment = fragment
|
||||
this.userCancelledFlowCallback = userCancelledFlowCallback
|
||||
|
||||
val disposables = LifecycleDisposable()
|
||||
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
|
||||
|
||||
disposables.bindTo(fragment.viewLifecycleOwner)
|
||||
disposables += registerErrorSource(errorSource)
|
||||
additionalSources.forEach { source ->
|
||||
disposables += registerErrorSource(source)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
errorDialog?.dismiss()
|
||||
fragment = null
|
||||
userCancelledFlowCallback = null
|
||||
}
|
||||
|
||||
private fun registerErrorSource(errorSource: DonationErrorSource): Disposable {
|
||||
return DonationError.getErrorsForSource(errorSource)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { error ->
|
||||
showErrorDialog(error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showErrorDialog(throwable: Throwable) {
|
||||
if (errorDialog != null) {
|
||||
Log.d(TAG, "Already displaying an error dialog. Skipping.", throwable, true)
|
||||
return
|
||||
}
|
||||
|
||||
if (throwable is DonationError.UserCancelledPaymentError) {
|
||||
Log.d(TAG, "User cancelled out of payment flow.", true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Displaying donation error dialog.", true)
|
||||
errorDialog = DonationErrorDialogs.show(
|
||||
fragment!!.requireContext(), throwable,
|
||||
object : DonationErrorDialogs.DialogCallback() {
|
||||
var tryCCAgain = false
|
||||
|
||||
override fun onTryCreditCardAgain(context: Context): DonationErrorParams.ErrorAction<Unit> {
|
||||
return DonationErrorParams.ErrorAction(
|
||||
label = R.string.DeclineCode__try,
|
||||
action = {
|
||||
tryCCAgain = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDialogDismissed() {
|
||||
errorDialog = null
|
||||
if (!tryCCAgain) {
|
||||
fragment!!.findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface UserCancelledFlowCallback {
|
||||
fun onUserCancelledPaymentFlow()
|
||||
}
|
||||
|
||||
interface Callback : UserCancelledFlowCallback {
|
||||
fun navigateToStripePaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToPayPalPaymentInProgress(gatewayRequest: GatewayRequest)
|
||||
fun navigateToCreditCardForm(gatewayRequest: GatewayRequest)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
|
||||
/**
|
||||
* Utilized in the 3DS and PayPal WebView fragments to handle WebView back navigation.
|
||||
*/
|
||||
class DonationWebViewOnBackPressedCallback(
|
||||
private val dismissAllowingStateLoss: () -> Unit,
|
||||
private val webView: WebView,
|
||||
) : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
} else {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,22 +7,30 @@ import android.view.WindowManager
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.fragment.app.setFragmentResultListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager.LayoutParams
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationCheckoutDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.stripe.StripePaymentInProgressViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
|
||||
import org.thoughtcrime.securesms.databinding.CreditCardFragmentBinding
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
|
||||
class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
@@ -31,8 +39,30 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
private val args: CreditCardFragmentArgs by navArgs()
|
||||
private val viewModel: CreditCardViewModel by viewModels()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private val stripePaymentViewModel: StripePaymentInProgressViewModel by navGraphViewModels(
|
||||
R.id.donate_to_signal,
|
||||
factoryProducer = {
|
||||
StripePaymentInProgressViewModel.Factory(requireListener<DonationPaymentComponent>().stripeRepository)
|
||||
}
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val errorSource: DonationErrorSource = when (args.request.donateToSignalType) {
|
||||
DonateToSignalType.ONE_TIME -> DonationErrorSource.BOOST
|
||||
DonateToSignalType.MONTHLY -> DonationErrorSource.SUBSCRIPTION
|
||||
DonateToSignalType.GIFT -> DonationErrorSource.GIFT
|
||||
}
|
||||
|
||||
DonationCheckoutDelegate.ErrorHandler().attach(this, null, errorSource)
|
||||
|
||||
setFragmentResultListener(StripePaymentInProgressFragment.REQUEST_KEY) { _, bundle ->
|
||||
val result: DonationProcessorActionResult = bundle.getParcelable(StripePaymentInProgressFragment.REQUEST_KEY)!!
|
||||
if (result.status == DonationProcessorActionResult.Status.SUCCESS) {
|
||||
findNavController().popBackStack()
|
||||
setFragmentResult(REQUEST_KEY, bundle)
|
||||
}
|
||||
}
|
||||
|
||||
binding.title.text = if (args.request.donateToSignalType == DonateToSignalType.MONTHLY) {
|
||||
getString(
|
||||
R.string.CreditCardFragment__donation_amount_s_per_month,
|
||||
@@ -54,7 +84,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
|
||||
binding.cardNumber.addTextChangedListener(CreditCardTextWatcher())
|
||||
|
||||
binding.cardNumber.setOnFocusChangeListener { v, hasFocus ->
|
||||
binding.cardNumber.setOnFocusChangeListener { _, hasFocus ->
|
||||
viewModel.onNumberFocusChanged(hasFocus)
|
||||
}
|
||||
|
||||
@@ -62,12 +92,12 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
viewModel.onCodeChanged(it?.toString() ?: "")
|
||||
})
|
||||
|
||||
binding.cardCvv.setOnFocusChangeListener { v, hasFocus ->
|
||||
binding.cardCvv.setOnFocusChangeListener { _, hasFocus ->
|
||||
viewModel.onCodeFocusChanged(hasFocus)
|
||||
}
|
||||
|
||||
binding.cardCvv.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE && binding.continueButton.isEnabled) {
|
||||
binding.continueButton.performClick()
|
||||
true
|
||||
} else {
|
||||
@@ -81,21 +111,18 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
|
||||
binding.cardExpiry.addTextChangedListener(CreditCardExpirationTextWatcher())
|
||||
|
||||
binding.cardExpiry.setOnFocusChangeListener { v, hasFocus ->
|
||||
binding.cardExpiry.setOnFocusChangeListener { _, hasFocus ->
|
||||
viewModel.onExpirationFocusChanged(hasFocus)
|
||||
}
|
||||
|
||||
binding.continueButton.setOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
|
||||
val resultBundle = bundleOf(
|
||||
REQUEST_KEY to CreditCardResult(
|
||||
args.request,
|
||||
viewModel.getCardData()
|
||||
stripePaymentViewModel.provideCardData(viewModel.getCardData())
|
||||
findNavController().safeNavigate(
|
||||
CreditCardFragmentDirections.actionCreditCardFragmentToStripePaymentInProgressFragment(
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION,
|
||||
args.request
|
||||
)
|
||||
)
|
||||
|
||||
setFragmentResult(REQUEST_KEY, resultBundle)
|
||||
}
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
@@ -196,7 +223,7 @@ class CreditCardFragment : Fragment(R.layout.credit_card_fragment) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
val REQUEST_KEY = "card.data"
|
||||
const val REQUEST_KEY = "card.result"
|
||||
|
||||
private val NO_ERROR = ErrorState(false, -1)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,21 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate.g
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.donations.PaymentSourceType
|
||||
|
||||
@Parcelize
|
||||
data class GatewayResponse(val gateway: Gateway, val request: GatewayRequest) : Parcelable {
|
||||
enum class Gateway {
|
||||
GOOGLE_PAY,
|
||||
PAYPAL,
|
||||
CREDIT_CARD
|
||||
CREDIT_CARD;
|
||||
|
||||
fun toPaymentSourceType(): PaymentSourceType {
|
||||
return when (this) {
|
||||
GOOGLE_PAY -> PaymentSourceType.Stripe.GooglePay
|
||||
PAYPAL -> PaymentSourceType.PayPal
|
||||
CREDIT_CARD -> PaymentSourceType.Stripe.CreditCard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
@@ -16,7 +17,6 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.NO_TINT
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.PayPalButton
|
||||
@@ -61,11 +61,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
space(12.dp)
|
||||
|
||||
when (args.request.donateToSignalType) {
|
||||
DonateToSignalType.MONTHLY -> presentMonthlyText()
|
||||
DonateToSignalType.ONE_TIME -> presentOneTimeText()
|
||||
DonateToSignalType.GIFT -> presentGiftText()
|
||||
}
|
||||
presentTitleAndSubtitle(requireContext(), args.request)
|
||||
|
||||
space(66.dp)
|
||||
|
||||
@@ -82,7 +78,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
)
|
||||
}
|
||||
|
||||
if (InAppDonations.isPayPalAvailable()) {
|
||||
if (state.isPayPalAvailable) {
|
||||
space(8.dp)
|
||||
|
||||
customPref(
|
||||
@@ -97,7 +93,7 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
)
|
||||
}
|
||||
|
||||
if (InAppDonations.isCreditCardAvailable()) {
|
||||
if (state.isCreditCardAvailable) {
|
||||
space(8.dp)
|
||||
|
||||
primaryButton(
|
||||
@@ -115,64 +111,72 @@ class GatewaySelectorBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentMonthlyText() {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
getString(R.string.GatewaySelectorBottomSheet__donate_s_month_to_signal, FiatMoneyUtil.format(resources, args.request.fiat)),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
getString(R.string.GatewaySelectorBottomSheet__get_a_s_badge, args.request.badge.name),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentOneTimeText() {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(resources, args.request.fiat)),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
resources.getQuantityString(R.plurals.GatewaySelectorBottomSheet__get_a_s_badge_for_d_days, 30, args.request.badge.name, 30),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentGiftText() {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(resources, args.request.fiat)),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.GatewaySelectorBottomSheet__send_a_gift_badge,
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "payment_checkout_mode"
|
||||
|
||||
fun DSLConfiguration.presentTitleAndSubtitle(context: Context, request: GatewayRequest) {
|
||||
when (request.donateToSignalType) {
|
||||
DonateToSignalType.MONTHLY -> presentMonthlyText(context, request)
|
||||
DonateToSignalType.ONE_TIME -> presentOneTimeText(context, request)
|
||||
DonateToSignalType.GIFT -> presentGiftText(context, request)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentMonthlyText(context: Context, request: GatewayRequest) {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__donate_s_month_to_signal, FiatMoneyUtil.format(context.resources, request.fiat)),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__get_a_s_badge, request.badge.name),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentOneTimeText(context: Context, request: GatewayRequest) {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(context.resources, request.fiat)),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.resources.getQuantityString(R.plurals.GatewaySelectorBottomSheet__get_a_s_badge_for_d_days, 30, request.badge.name, 30),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun DSLConfiguration.presentGiftText(context: Context, request: GatewayRequest) {
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
context.getString(R.string.GatewaySelectorBottomSheet__donate_s_to_signal, FiatMoneyUtil.format(context.resources, request.fiat)),
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.TitleLargeModifier
|
||||
)
|
||||
)
|
||||
space(6.dp)
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.GatewaySelectorBottomSheet__send_a_gift_badge,
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.BodyLargeModifier,
|
||||
DSLSettingsText.ColorModifier(ContextCompat.getColor(context, R.color.signal_colorOnSurfaceVariant))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,7 @@ import org.thoughtcrime.securesms.badges.models.Badge
|
||||
|
||||
data class GatewaySelectorState(
|
||||
val badge: Badge,
|
||||
val isGooglePayAvailable: Boolean = false
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val isPayPalAvailable: Boolean = false,
|
||||
val isCreditCardAvailable: Boolean = false
|
||||
)
|
||||
|
||||
@@ -5,7 +5,10 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class GatewaySelectorViewModel(
|
||||
@@ -13,7 +16,14 @@ class GatewaySelectorViewModel(
|
||||
private val repository: StripeRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = RxStore(GatewaySelectorState(args.request.badge))
|
||||
private val store = RxStore(
|
||||
GatewaySelectorState(
|
||||
badge = args.request.badge,
|
||||
isGooglePayAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.GooglePay, args.request.donateToSignalType),
|
||||
isCreditCardAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.Stripe.CreditCard, args.request.donateToSignalType),
|
||||
isPayPalAvailable = InAppDonations.isPaymentSourceAvailable(PaymentSourceType.PayPal, args.request.donateToSignalType)
|
||||
)
|
||||
)
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
val state = store.stateFlowable
|
||||
@@ -30,9 +40,11 @@ class GatewaySelectorViewModel(
|
||||
private fun checkIfGooglePayIsAvailable() {
|
||||
disposables += repository.isGooglePayAvailable().subscribeBy(
|
||||
onComplete = {
|
||||
SignalStore.donationsValues().isGooglePayReady = true
|
||||
store.update { it.copy(isGooglePayAvailable = true) }
|
||||
},
|
||||
onError = {
|
||||
SignalStore.donationsValues().isGooglePayReady = false
|
||||
store.update { it.copy(isGooglePayAvailable = false) }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import android.content.DialogInterface
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewaySelectorBottomSheet.Companion.presentTitleAndSubtitle
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
|
||||
/**
|
||||
* Bottom sheet for final order confirmation from PayPal
|
||||
*/
|
||||
class PayPalCompleteOrderBottomSheet : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
companion object {
|
||||
const val REQUEST_KEY = "complete_order"
|
||||
}
|
||||
|
||||
private var didConfirmOrder = false
|
||||
private val args: PayPalCompleteOrderBottomSheetArgs by navArgs()
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgeDisplay112.register(adapter)
|
||||
PayPalCompleteOrderPaymentItem.register(adapter)
|
||||
|
||||
adapter.submitList(getConfiguration().toMappingModelList())
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to didConfirmOrder))
|
||||
}
|
||||
|
||||
private fun getConfiguration(): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
BadgeDisplay112.Model(
|
||||
badge = args.request.badge,
|
||||
withDisplayText = false
|
||||
)
|
||||
)
|
||||
|
||||
space(12.dp)
|
||||
|
||||
presentTitleAndSubtitle(requireContext(), args.request)
|
||||
|
||||
space(24.dp)
|
||||
|
||||
customPref(PayPalCompleteOrderPaymentItem.Model())
|
||||
|
||||
space(82.dp)
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.PaypalCompleteOrderBottomSheet__donate),
|
||||
onClick = {
|
||||
didConfirmOrder = true
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
)
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(android.R.string.cancel),
|
||||
onClick = {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
)
|
||||
|
||||
space(16.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate.paypal
|
||||
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder.SimpleViewHolder
|
||||
|
||||
/**
|
||||
* Line item on the PayPal order confirmation screen.
|
||||
*/
|
||||
object PayPalCompleteOrderPaymentItem {
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::SimpleViewHolder, R.layout.paypal_complete_order_payment_item))
|
||||
}
|
||||
|
||||
class Model : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = true
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,18 @@ import android.view.View
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentDialog
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.PayPalRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationWebViewOnBackPressedCallback
|
||||
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
@@ -47,10 +51,20 @@ class PayPalConfirmationDialogFragment : DialogFragment(R.layout.donation_webvie
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.webView.webViewClient = PayPalWebClient()
|
||||
val client = PayPalWebClient()
|
||||
viewLifecycleOwner.lifecycle.addObserver(client)
|
||||
binding.webView.webViewClient = client
|
||||
binding.webView.settings.javaScriptEnabled = true
|
||||
binding.webView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
binding.webView.loadUrl(args.uri.toString())
|
||||
|
||||
(requireDialog() as ComponentDialog).onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
DonationWebViewOnBackPressedCallback(
|
||||
this::dismissAllowingStateLoss,
|
||||
binding.webView
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
@@ -59,21 +73,31 @@ class PayPalConfirmationDialogFragment : DialogFragment(R.layout.donation_webvie
|
||||
setFragmentResult(REQUEST_KEY, result ?: Bundle())
|
||||
}
|
||||
|
||||
private inner class PayPalWebClient : WebViewClient() {
|
||||
private inner class PayPalWebClient : WebViewClient(), DefaultLifecycleObserver {
|
||||
|
||||
private var isDestroyed = false
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
isDestroyed = true
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
if (!isFinished) {
|
||||
if (!isDestroyed) {
|
||||
binding.progress.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageCommitVisible(view: WebView?, url: String?) {
|
||||
if (!isFinished) {
|
||||
if (!isDestroyed) {
|
||||
binding.progress.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
if (isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
if (url?.startsWith(PayPalRepository.ONE_TIME_RETURN_URL) == true) {
|
||||
val confirmationResult = PayPalConfirmationResult.fromUrl(url)
|
||||
if (confirmationResult != null) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||
@@ -57,7 +58,7 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
viewModel.onBeginNewAction()
|
||||
when (args.action) {
|
||||
DonationProcessorAction.PROCESS_NEW_DONATION -> {
|
||||
viewModel.processNewDonation(args.request, this::routeToOneTimeConfirmation, this::routeToMonthlyConfirmation)
|
||||
viewModel.processNewDonation(args.request, this::oneTimeConfirmationPipeline, this::monthlyConfirmationPipeline)
|
||||
}
|
||||
DonationProcessorAction.UPDATE_SUBSCRIPTION -> {
|
||||
viewModel.updateSubscription(args.request)
|
||||
@@ -110,6 +111,14 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
}
|
||||
}
|
||||
|
||||
private fun oneTimeConfirmationPipeline(createPaymentIntentResponse: PayPalCreatePaymentIntentResponse): Single<PayPalConfirmationResult> {
|
||||
return routeToOneTimeConfirmation(createPaymentIntentResponse)
|
||||
}
|
||||
|
||||
private fun monthlyConfirmationPipeline(createPaymentIntentResponse: PayPalCreatePaymentMethodResponse): Single<PayPalPaymentMethodId> {
|
||||
return routeToMonthlyConfirmation(createPaymentIntentResponse)
|
||||
}
|
||||
|
||||
private fun routeToOneTimeConfirmation(createPaymentIntentResponse: PayPalCreatePaymentIntentResponse): Single<PayPalConfirmationResult> {
|
||||
return Single.create<PayPalConfirmationResult> { emitter ->
|
||||
val listener = FragmentResultListener { _, bundle ->
|
||||
@@ -117,10 +126,11 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result != null) {
|
||||
emitter.onSuccess(result)
|
||||
} else {
|
||||
emitter.onError(Exception("User did not complete paypal confirmation."))
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.request.donateToSignalType.toErrorSource()))
|
||||
}
|
||||
}
|
||||
|
||||
parentFragmentManager.clearFragmentResult(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
parentFragmentManager.setFragmentResultListener(PayPalConfirmationDialogFragment.REQUEST_KEY, this, listener)
|
||||
|
||||
findNavController().safeNavigate(
|
||||
@@ -130,6 +140,8 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
)
|
||||
|
||||
emitter.setCancellable {
|
||||
Log.d(TAG, "Clearing one-time confirmation result listener.")
|
||||
parentFragmentManager.clearFragmentResult(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
parentFragmentManager.clearFragmentResultListener(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
}
|
||||
}.subscribeOn(AndroidSchedulers.mainThread()).observeOn(Schedulers.io())
|
||||
@@ -138,14 +150,15 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
private fun routeToMonthlyConfirmation(createPaymentIntentResponse: PayPalCreatePaymentMethodResponse): Single<PayPalPaymentMethodId> {
|
||||
return Single.create<PayPalPaymentMethodId> { emitter ->
|
||||
val listener = FragmentResultListener { _, bundle ->
|
||||
val result: Boolean = bundle.getBoolean(REQUEST_KEY)
|
||||
val result: Boolean = bundle.getBoolean(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
if (result) {
|
||||
emitter.onSuccess(PayPalPaymentMethodId(createPaymentIntentResponse.token))
|
||||
} else {
|
||||
emitter.onError(Exception("User did not confirm paypal setup."))
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.request.donateToSignalType.toErrorSource()))
|
||||
}
|
||||
}
|
||||
|
||||
parentFragmentManager.clearFragmentResult(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
parentFragmentManager.setFragmentResultListener(PayPalConfirmationDialogFragment.REQUEST_KEY, this, listener)
|
||||
|
||||
findNavController().safeNavigate(
|
||||
@@ -155,8 +168,37 @@ class PayPalPaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
)
|
||||
|
||||
emitter.setCancellable {
|
||||
Log.d(TAG, "Clearing monthly confirmation result listener.")
|
||||
parentFragmentManager.clearFragmentResult(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
parentFragmentManager.clearFragmentResultListener(PayPalConfirmationDialogFragment.REQUEST_KEY)
|
||||
}
|
||||
}.subscribeOn(AndroidSchedulers.mainThread()).observeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun <T : Any> displayCompleteOrderSheet(confirmationData: T): Single<T> {
|
||||
return Single.create<T> { emitter ->
|
||||
val listener = FragmentResultListener { _, bundle ->
|
||||
val result: Boolean = bundle.getBoolean(PayPalCompleteOrderBottomSheet.REQUEST_KEY)
|
||||
if (result) {
|
||||
Log.d(TAG, "User confirmed order. Continuing...")
|
||||
emitter.onSuccess(confirmationData)
|
||||
} else {
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.request.donateToSignalType.toErrorSource()))
|
||||
}
|
||||
}
|
||||
|
||||
parentFragmentManager.clearFragmentResult(PayPalCompleteOrderBottomSheet.REQUEST_KEY)
|
||||
parentFragmentManager.setFragmentResultListener(PayPalCompleteOrderBottomSheet.REQUEST_KEY, this, listener)
|
||||
|
||||
findNavController().safeNavigate(
|
||||
PayPalPaymentInProgressFragmentDirections.actionPaypalPaymentInProgressFragmentToPaypalCompleteOrderBottomSheet(args.request)
|
||||
)
|
||||
|
||||
emitter.setCancellable {
|
||||
Log.d(TAG, "Clearing complete order result listener.")
|
||||
parentFragmentManager.clearFragmentResult(PayPalCompleteOrderBottomSheet.REQUEST_KEY)
|
||||
parentFragmentManager.clearFragmentResultListener(PayPalCompleteOrderBottomSheet.REQUEST_KEY)
|
||||
}
|
||||
}.subscribeOn(AndroidSchedulers.mainThread()).observeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.View
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.ComponentDialog
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
@@ -15,6 +16,7 @@ import androidx.navigation.fragment.navArgs
|
||||
import org.signal.donations.StripeIntentAccessor
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationWebViewOnBackPressedCallback
|
||||
import org.thoughtcrime.securesms.databinding.DonationWebviewFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
@@ -47,6 +49,14 @@ class Stripe3DSDialogFragment : DialogFragment(R.layout.donation_webview_fragmen
|
||||
binding.webView.settings.javaScriptEnabled = true
|
||||
binding.webView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
binding.webView.loadUrl(args.uri.toString())
|
||||
|
||||
(requireDialog() as ComponentDialog).onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
DonationWebViewOnBackPressedCallback(
|
||||
this::dismissAllowingStateLoss,
|
||||
binding.webView
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.DonationP
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorAction
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorActionResult
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonationProcessorStage
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
|
||||
import org.thoughtcrime.securesms.databinding.DonationInProgressFragmentBinding
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
@@ -128,7 +129,7 @@ class StripePaymentInProgressFragment : DialogFragment(R.layout.donation_in_prog
|
||||
if (result != null) {
|
||||
emitter.onSuccess(result)
|
||||
} else {
|
||||
emitter.onError(Exception("User did not complete 3DS Authorization."))
|
||||
emitter.onError(DonationError.UserCancelledPaymentError(args.request.donateToSignalType.toErrorSource()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ sealed class DonationError(val source: DonationErrorSource, cause: Throwable) :
|
||||
class RequestTokenError(source: DonationErrorSource, cause: Throwable) : GooglePayError(source, cause)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilized when the user cancels the payment flow, by either exiting a WebView or not confirming on the complete order sheet.
|
||||
*/
|
||||
class UserCancelledPaymentError(source: DonationErrorSource) : DonationError(source, Exception("User cancelled payment."))
|
||||
|
||||
/**
|
||||
* Gifting recipient validation errors, which occur before the user could be charged for a gift.
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
object DonationErrorNotifications {
|
||||
fun displayErrorNotification(context: Context, donationError: DonationError) {
|
||||
val parameters = DonationErrorParams.create(context, donationError, NotificationCallback)
|
||||
val notification = NotificationCompat.Builder(context, NotificationChannels.FAILURES)
|
||||
val notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(context.getString(parameters.title))
|
||||
.setContentText(context.getString(parameters.message)).apply {
|
||||
|
||||
@@ -90,8 +90,8 @@ class DonationErrorParams<V> private constructor(
|
||||
|
||||
private fun <V> getDeclinedErrorParams(context: Context, declinedError: DonationError.PaymentSetupError.StripeDeclinedError, callback: Callback<V>): DonationErrorParams<V> {
|
||||
val getStripeDeclineCodePositiveActionParams: (Context, Callback<V>, Int) -> DonationErrorParams<V> = when (declinedError.method) {
|
||||
PaymentSourceType.Stripe.GooglePay -> this::getTryCreditCardAgainParams
|
||||
PaymentSourceType.Stripe.CreditCard -> this::getGoToGooglePayParams
|
||||
PaymentSourceType.Stripe.CreditCard -> this::getTryCreditCardAgainParams
|
||||
PaymentSourceType.Stripe.GooglePay -> this::getGoToGooglePayParams
|
||||
}
|
||||
|
||||
return when (declinedError.declineCode) {
|
||||
@@ -99,8 +99,8 @@ class DonationErrorParams<V> private constructor(
|
||||
StripeDeclineCode.Code.APPROVE_WITH_ID -> getStripeDeclineCodePositiveActionParams(
|
||||
context, callback,
|
||||
when (declinedError.method) {
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again
|
||||
PaymentSourceType.Stripe.CreditCard -> R.string.DeclineCode__verify_your_card_details_are_correct_and_try_again
|
||||
PaymentSourceType.Stripe.GooglePay -> R.string.DeclineCode__verify_your_payment_method_is_up_to_date_in_google_pay_and_try_again
|
||||
}
|
||||
)
|
||||
StripeDeclineCode.Code.CALL_ISSUER -> getStripeDeclineCodePositiveActionParams(
|
||||
|
||||
@@ -77,7 +77,7 @@ class ManageDonationsViewModel(
|
||||
disposables += SubscriptionRedemptionJobWatcher.watch().subscribeBy { jobStateOptional ->
|
||||
store.update { manageDonationsState ->
|
||||
manageDonationsState.copy(
|
||||
subscriptionRedemptionState = jobStateOptional.map { jobState ->
|
||||
subscriptionRedemptionState = jobStateOptional.map { jobState: JobTracker.JobState ->
|
||||
when (jobState) {
|
||||
JobTracker.JobState.PENDING -> ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS
|
||||
JobTracker.JobState.RUNNING -> ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.detail
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
@@ -16,6 +15,7 @@ import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.concurrent.SimpleTask
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.SignalProgressDialog
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
@@ -31,7 +31,7 @@ import java.util.Locale
|
||||
|
||||
class DonationReceiptDetailFragment : DSLSettingsFragment(layoutId = R.layout.donation_receipt_detail_fragment) {
|
||||
|
||||
private lateinit var progressDialog: ProgressDialog
|
||||
private lateinit var progressDialog: SignalProgressDialog
|
||||
|
||||
private val viewModel: DonationReceiptDetailViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
@@ -63,8 +63,7 @@ class DonationReceiptDetailFragment : DSLSettingsFragment(layoutId = R.layout.do
|
||||
}
|
||||
|
||||
private fun renderPng(record: DonationReceiptRecord, subscriptionName: String) {
|
||||
progressDialog = ProgressDialog(requireContext())
|
||||
progressDialog.show()
|
||||
progressDialog = SignalProgressDialog.show(requireContext())
|
||||
|
||||
val today: String = DateUtils.formatDateWithDayOfWeek(Locale.getDefault(), System.currentTimeMillis())
|
||||
val amount: String = FiatMoneyUtil.format(resources, record.amount)
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.receipts
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getSubscriptionLevels
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -13,10 +14,10 @@ class DonationReceiptDetailRepository {
|
||||
.fromCallable {
|
||||
ApplicationDependencies
|
||||
.getDonationsService()
|
||||
.getSubscriptionLevels(Locale.getDefault())
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
.flatMap { it.flattenResult() }
|
||||
.map { it.levels[subscriptionLevel.toString()] ?: throw Exception("Subscription level $subscriptionLevel not found") }
|
||||
.map { it.getSubscriptionLevels()[subscriptionLevel] ?: throw Exception("Subscription level $subscriptionLevel not found") }
|
||||
.map { it.name }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ class DonationReceiptListPageFragment : Fragment(R.layout.donation_receipt_list_
|
||||
private fun getBadgeForRecord(record: DonationReceiptRecord, badges: List<DonationReceiptBadge>): Badge? {
|
||||
return when (record.type) {
|
||||
DonationReceiptRecord.Type.BOOST -> badges.firstOrNull { it.type == DonationReceiptRecord.Type.BOOST }?.badge
|
||||
DonationReceiptRecord.Type.GIFT -> badges.firstOrNull { it.type == DonationReceiptRecord.Type.GIFT }?.badge
|
||||
else -> badges.firstOrNull { it.level == record.subscriptionLevel }?.badge
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,35 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.receipts.list
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getBoostBadges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.getSubscriptionLevels
|
||||
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import java.util.Locale
|
||||
|
||||
class DonationReceiptListRepository {
|
||||
fun getBadges(): Single<List<DonationReceiptBadge>> {
|
||||
val boostBadges: Single<List<DonationReceiptBadge>> = Single
|
||||
.fromCallable {
|
||||
ApplicationDependencies.getDonationsService()
|
||||
.getBoostBadge(Locale.getDefault())
|
||||
}
|
||||
.map { response ->
|
||||
if (response.result.isPresent) {
|
||||
listOf(DonationReceiptBadge(DonationReceiptRecord.Type.BOOST, -1, Badges.fromServiceBadge(response.result.get())))
|
||||
} else {
|
||||
emptyList()
|
||||
return Single.fromCallable {
|
||||
ApplicationDependencies.getDonationsService()
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}.map { response ->
|
||||
if (response.result.isPresent) {
|
||||
val config = response.result.get()
|
||||
val boostBadge = DonationReceiptBadge(DonationReceiptRecord.Type.BOOST, -1, config.getBoostBadges().first())
|
||||
val giftBadge = DonationReceiptBadge(DonationReceiptRecord.Type.GIFT, -1, config.getGiftBadges().first())
|
||||
val subBadges = config.getSubscriptionLevels().map {
|
||||
DonationReceiptBadge(
|
||||
level = it.key,
|
||||
badge = Badges.fromServiceBadge(it.value.badge),
|
||||
type = DonationReceiptRecord.Type.RECURRING
|
||||
)
|
||||
}
|
||||
subBadges + boostBadge + giftBadge
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val subBadges: Single<List<DonationReceiptBadge>> = Single
|
||||
.fromCallable { ApplicationDependencies.getDonationsService().getSubscriptionLevels(Locale.getDefault()) }
|
||||
.map { response ->
|
||||
if (response.result.isPresent) {
|
||||
response.result.get().levels.map {
|
||||
DonationReceiptBadge(
|
||||
level = it.key.toInt(),
|
||||
badge = Badges.fromServiceBadge(it.value.badge),
|
||||
type = DonationReceiptRecord.Type.RECURRING
|
||||
)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
return boostBadges.zipWith(subBadges) { a, b -> a + b }.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -141,6 +142,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
private lateinit var addToGroupStoryDelegate: AddToGroupStoryDelegate
|
||||
|
||||
private val navController get() = Navigation.findNavController(requireView())
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
@@ -256,7 +258,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.events.observe(viewLifecycleOwner) { event ->
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
lifecycleDisposable += viewModel.events.subscribe { event ->
|
||||
@Exhaustive
|
||||
when (event) {
|
||||
is ConversationSettingsEvent.AddToAGroup -> handleAddToAGroup(event)
|
||||
|
||||
@@ -6,8 +6,12 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
@@ -24,7 +28,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import java.util.Optional
|
||||
@@ -44,12 +47,12 @@ sealed class ConversationSettingsViewModel(
|
||||
specificSettingsState = specificSettingsState
|
||||
)
|
||||
)
|
||||
protected val internalEvents = SingleLiveEvent<ConversationSettingsEvent>()
|
||||
protected val internalEvents: Subject<ConversationSettingsEvent> = PublishSubject.create()
|
||||
|
||||
private val sharedMediaUpdateTrigger = MutableLiveData(Unit)
|
||||
|
||||
val state: LiveData<ConversationSettingsState> = store.stateLiveData
|
||||
val events: LiveData<ConversationSettingsEvent> = internalEvents
|
||||
val events: Observable<ConversationSettingsEvent> = internalEvents.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
protected val disposable = CompositeDisposable()
|
||||
|
||||
@@ -210,7 +213,7 @@ sealed class ConversationSettingsViewModel(
|
||||
|
||||
override fun onAddToGroup() {
|
||||
repository.getGroupMembership(recipientId) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.AddToAGroup(recipientId, it))
|
||||
internalEvents.onNext(ConversationSettingsEvent.AddToAGroup(recipientId, it))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,7 +407,7 @@ sealed class ConversationSettingsViewModel(
|
||||
repository.getGroupCapacity(groupId) { capacityResult ->
|
||||
if (capacityResult.getRemainingCapacity() > 0) {
|
||||
|
||||
internalEvents.postValue(
|
||||
internalEvents.onNext(
|
||||
ConversationSettingsEvent.AddMembersToGroup(
|
||||
groupId,
|
||||
capacityResult.getSelectionWarning(),
|
||||
@@ -414,7 +417,7 @@ sealed class ConversationSettingsViewModel(
|
||||
)
|
||||
)
|
||||
} else {
|
||||
internalEvents.postValue(ConversationSettingsEvent.ShowGroupHardLimitDialog)
|
||||
internalEvents.onNext(ConversationSettingsEvent.ShowGroupHardLimitDialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -426,14 +429,14 @@ sealed class ConversationSettingsViewModel(
|
||||
when (it) {
|
||||
is GroupAddMembersResult.Success -> {
|
||||
if (it.newMembersInvited.isNotEmpty()) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.ShowGroupInvitesSentDialog(it.newMembersInvited))
|
||||
internalEvents.onNext(ConversationSettingsEvent.ShowGroupInvitesSentDialog(it.newMembersInvited))
|
||||
}
|
||||
|
||||
if (it.numberOfMembersAdded > 0) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.ShowMembersAdded(it.numberOfMembersAdded))
|
||||
internalEvents.onNext(ConversationSettingsEvent.ShowMembersAdded(it.numberOfMembersAdded))
|
||||
}
|
||||
}
|
||||
is GroupAddMembersResult.Failure -> internalEvents.postValue(ConversationSettingsEvent.ShowAddMembersToGroupError(it.reason))
|
||||
is GroupAddMembersResult.Failure -> internalEvents.onNext(ConversationSettingsEvent.ShowAddMembersToGroupError(it.reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -468,7 +471,7 @@ sealed class ConversationSettingsViewModel(
|
||||
|
||||
override fun initiateGroupUpgrade() {
|
||||
repository.getExternalPossiblyMigratedGroupRecipientId(groupId) {
|
||||
internalEvents.postValue(ConversationSettingsEvent.InitiateGroupMigration(it))
|
||||
internalEvents.onNext(ConversationSettingsEvent.InitiateGroupMigration(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class CustomNotificationsSettingsFragment : DSLSettingsFragment(R.string.CustomN
|
||||
title = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__customize),
|
||||
summary = DSLSettingsText.from(R.string.CustomNotificationsDialogFragment__change_sound_and_vibration),
|
||||
isEnabled = state.controlsEnabled,
|
||||
onClick = { NotificationChannels.getInstance().openChannelSettings(state.recipient!!.notificationChannel!!, ConversationUtil.getShortcutId(state.recipient)) }
|
||||
onClick = { NotificationChannels.getInstance().openChannelSettings(requireActivity(), state.recipient!!.notificationChannel!!, ConversationUtil.getShortcutId(state.recipient)) }
|
||||
)
|
||||
} else {
|
||||
clickPref(
|
||||
|
||||
@@ -9,7 +9,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class CustomNotificationsSettingsViewModel(
|
||||
@@ -35,7 +34,7 @@ class CustomNotificationsSettingsViewModel(
|
||||
RecipientTable.VibrateState.ENABLED -> true
|
||||
RecipientTable.VibrateState.DISABLED -> false
|
||||
},
|
||||
showCallingOptions = recipient.isRegistered && (!recipient.isGroup || FeatureFlags.groupCallRinging()),
|
||||
showCallingOptions = recipient.isRegistered,
|
||||
callSound = recipient.callRingtone,
|
||||
callVibrateState = recipient.callVibrate
|
||||
)
|
||||
|
||||
@@ -40,7 +40,7 @@ class VoiceNoteNotificationManager {
|
||||
{
|
||||
this.context = context;
|
||||
controller = new MediaControllerCompat(context, token);
|
||||
notificationManager = new PlayerNotificationManager.Builder(context, NOW_PLAYING_NOTIFICATION_ID, NotificationChannels.VOICE_NOTES)
|
||||
notificationManager = new PlayerNotificationManager.Builder(context, NOW_PLAYING_NOTIFICATION_ID, NotificationChannels.getInstance().VOICE_NOTES)
|
||||
.setChannelNameResourceId(R.string.NotificationChannel_voice_notes)
|
||||
.setMediaDescriptionAdapter(new DescriptionAdapter())
|
||||
.setNotificationListener(listener)
|
||||
|
||||
@@ -31,7 +31,7 @@ public final class GroupCallSafetyNumberChangeNotificationUtil {
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntentFlags.mutable());
|
||||
|
||||
Notification safetyNumberChangeNotification = new NotificationCompat.Builder(context, NotificationChannels.CALLS)
|
||||
Notification safetyNumberChangeNotification = new NotificationCompat.Builder(context, NotificationChannels.getInstance().CALLS)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(recipient.getDisplayName(context))
|
||||
.setContentText(context.getString(R.string.GroupCallSafetyNumberChangeNotification__someone_has_joined_this_call_with_a_safety_number_that_has_changed))
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.ResizeAnimation;
|
||||
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
@@ -147,6 +148,7 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
};
|
||||
|
||||
private CallParticipantsViewState lastState;
|
||||
private ContactPhoto previousLocalAvatar;
|
||||
|
||||
public WebRtcCallView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
@@ -506,11 +508,16 @@ public class WebRtcCallView extends ConstraintLayout {
|
||||
largeLocalRenderNoVideo.setVisibility(View.VISIBLE);
|
||||
largeLocalRenderNoVideoAvatar.setVisibility(View.VISIBLE);
|
||||
|
||||
GlideApp.with(getContext().getApplicationContext())
|
||||
.load(new ProfileContactPhoto(localCallParticipant.getRecipient()))
|
||||
.transform(new CenterCrop(), new BlurTransformation(getContext(), 0.25f, BlurTransformation.MAX_RADIUS))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(largeLocalRenderNoVideoAvatar);
|
||||
ContactPhoto localAvatar = new ProfileContactPhoto(localCallParticipant.getRecipient());
|
||||
|
||||
if (!localAvatar.equals(previousLocalAvatar)) {
|
||||
previousLocalAvatar = localAvatar;
|
||||
GlideApp.with(getContext().getApplicationContext())
|
||||
.load(localAvatar)
|
||||
.transform(new CenterCrop(), new BlurTransformation(getContext(), 0.25f, BlurTransformation.MAX_RADIUS))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(largeLocalRenderNoVideoAvatar);
|
||||
}
|
||||
|
||||
smallLocalRenderFrame.setVisibility(View.GONE);
|
||||
break;
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.annotation.Px;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
|
||||
|
||||
import java.util.Set;
|
||||
@@ -217,7 +216,7 @@ public final class WebRtcControls {
|
||||
}
|
||||
|
||||
boolean displayRingToggle() {
|
||||
return FeatureFlags.groupCallRinging() && isPreJoin() && isGroupCall() && !hasAtLeastOneRemote;
|
||||
return isPreJoin() && isGroupCall() && !hasAtLeastOneRemote;
|
||||
}
|
||||
|
||||
private boolean isError() {
|
||||
|
||||
@@ -241,7 +241,9 @@ class ContactSearchPagedDataSource(
|
||||
}
|
||||
}
|
||||
|
||||
private fun canSendToGroup(groupRecord: GroupRecord): Boolean {
|
||||
private fun canSendToGroup(groupRecord: GroupRecord?): Boolean {
|
||||
if (groupRecord == null) return false
|
||||
|
||||
return if (groupRecord.isAnnouncementGroup) {
|
||||
groupRecord.isAdmin(Recipient.self())
|
||||
} else {
|
||||
|
||||
@@ -57,6 +57,7 @@ open class ConversationActivity : PassphraseRequiredActivity(), ConversationPare
|
||||
replaceFragment(intent!!)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
|
||||
@@ -54,15 +54,12 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CachedInflater;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ProjectionList;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -111,7 +108,6 @@ public class ConversationAdapter
|
||||
|
||||
private final Set<MultiselectPart> selected;
|
||||
private final Calendar calendar;
|
||||
private final MessageDigest digest;
|
||||
|
||||
private String searchQuery;
|
||||
private ConversationMessage recordToPulse;
|
||||
@@ -154,7 +150,6 @@ public class ConversationAdapter
|
||||
this.recipient = recipient;
|
||||
this.selected = new HashSet<>();
|
||||
this.calendar = Calendar.getInstance();
|
||||
this.digest = getMessageDigestOrThrow();
|
||||
this.hasWallpaper = recipient.hasWallpaper();
|
||||
this.isMessageRequestAccepted = true;
|
||||
this.colorizer = colorizer;
|
||||
@@ -487,7 +482,7 @@ public class ConversationAdapter
|
||||
/**
|
||||
* Momentarily highlights a mention at the requested position.
|
||||
*/
|
||||
void pulseAtPosition(int position) {
|
||||
public void pulseAtPosition(int position) {
|
||||
if (position >= 0 && position < getItemCount()) {
|
||||
int correctedPosition = isHeaderPosition(position) ? position + 1 : position;
|
||||
|
||||
@@ -599,14 +594,6 @@ public class ConversationAdapter
|
||||
}
|
||||
}
|
||||
|
||||
private static MessageDigest getMessageDigestOrThrow() {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable ConversationMessage getLastVisibleConversationMessage(int position) {
|
||||
try {
|
||||
return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0));
|
||||
|
||||
@@ -1131,7 +1131,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
MultiselectForwardFragmentArgs.create(requireContext(),
|
||||
multiselectParts,
|
||||
args -> MultiselectForwardFragment.showBottomSheet(getChildFragmentManager(),
|
||||
args.withSendButtonTint(listener.getSendButtonTint())));
|
||||
args));
|
||||
}
|
||||
|
||||
private void handleResendMessage(final MessageRecord message) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user