Compare commits

..

125 Commits

Author SHA1 Message Date
Greyson Parrelli
9846517075 Bump version to 6.6.1 2022-12-21 13:40:10 -05:00
Greyson Parrelli
0f1cc03dc0 Updated language translations. 2022-12-21 13:40:10 -05:00
Nicholas Tinsley
0e5031ab45 Revert "Switch to BT mic if available for voice memo recording."
This reverts commit 9f6eb142d2.
2022-12-21 13:25:56 -05:00
Greyson Parrelli
0e4926b5ec Bump version to 6.6.0 2022-12-19 18:42:05 -05:00
Greyson Parrelli
a25e7c6d3e Updated language translations. 2022-12-19 18:38:42 -05:00
Cody Henthorne
4081ac2a83 Fix video controls becoming unresponsive after quickly paging. 2022-12-19 14:30:37 -05:00
Alex Hart
98a528f595 Fix recording progress bar when animations are scaled. 2022-12-19 12:46:13 -04:00
Nicholas
680325b5ee Increase MediaPreviewV2 lifecycle logging. 2022-12-16 16:12:11 -05:00
Nicholas
16668574a9 Separate message for media decode failure. 2022-12-16 15:32:59 -05:00
Rashad Sookram
0d8f6de4c1 Refactor bandwidth mode setting. 2022-12-16 15:22:04 -05:00
Alex Hart
4c0a98d526 Add nullability check to video capture callback.
Fixes #12666
2022-12-16 15:22:04 -05:00
Greyson Parrelli
10f78d5daa Change spinner to lazily read database stuff.
Otherwise you get into situations where Spinner will force DB accesses
super early during Application#onCreate on the main thread, which can be
bad when testing large DB migrations.
2022-12-16 15:22:04 -05:00
Cody Henthorne
3ce5a7da67 Fix emoji toggle behavior when in emoji search mode.
When in emoji search, toggle would be set to "emoji" state or
act like in "emoji" state. Fix is to show "keyboard" state still
when in emoji search.
2022-12-16 15:22:04 -05:00
Greyson Parrelli
4d47b9c594 Round Spinner timings to 3 decimal places. 2022-12-16 15:22:04 -05:00
Nicholas
9f6eb142d2 Switch to BT mic if available for voice memo recording.
Addresses #12016.
2022-12-16 15:22:04 -05:00
Nicholas
0e08b4ee26 Correctly animate deletion when attaching multiple media. 2022-12-16 15:22:04 -05:00
Cody Henthorne
9b85907918 Fix flicker of local avatar in call view. 2022-12-16 15:22:04 -05:00
Cody Henthorne
6463dca2c6 Fix media selection dismissing when deselecting last item. 2022-12-16 15:22:04 -05:00
Alex Hart
498b7fee69 Remove SingleLiveEvent from EditAboutViewModel. 2022-12-16 15:22:04 -05:00
Cody Henthorne
3478e13d38 Fix progress dialog deprecation warnings.
Moves everything under our own class and ignores the deprecation. Also
gives us future ability to re-style all blocking UI dialogs in the
future for mat3 compat.
2022-12-16 15:22:04 -05:00
Alex Hart
5f0d37739a Remove SLE from EditProxyViewModel. 2022-12-16 15:22:04 -05:00
Cody Henthorne
c5b4f44ab8 Fix various compiler warnings. 2022-12-16 15:22:04 -05:00
Alex Hart
819c9f61dc Remove SingleLiveEvent from BlockedUsersActivity. 2022-12-16 15:22:04 -05:00
Alex Hart
4f167feaf5 Handle deprecated connectivity intent filter. 2022-12-16 15:22:04 -05:00
Alex Hart
de558bc87c Remove SingleLiveEvent from ConversationSettingsViewModel. 2022-12-16 15:22:04 -05:00
Alex Hart
4a5a65ff6c Remove usage of SingleLiveEvent from MediaCaptureViewModel. 2022-12-16 15:22:04 -05:00
Cody Henthorne
c56e63d62f Convert OutgoingMediaMessage and it's couterparts to kotlin. 2022-12-16 15:22:04 -05:00
Nicholas
8cd9a3cabe Map platform WIRED_HEADPHONES to our WIRED_HEADSET.
Fixes #12622.
2022-12-16 15:22:04 -05:00
Alex Hart
3a8c324c12 Clean up a bunch of warnings. 2022-12-16 15:22:04 -05:00
Cody Henthorne
ff882edeae Enable kotlin for libsignal-service project and convert SignalServiceDataMessage. 2022-12-16 15:22:04 -05:00
Cody Henthorne
fb0aa55cbb Fix instrumentation tests by forcing channel id usage to init channels. 2022-12-16 15:22:04 -05:00
Alex Hart
51015dc898 Clean up warnings in Gradle file. 2022-12-16 15:22:04 -05:00
Cody Henthorne
4af40e7861 Bump version to 6.5.6 2022-12-16 12:47:55 -05:00
Cody Henthorne
24fcc0c3b0 Updated language translations. 2022-12-16 12:40:19 -05:00
Nicholas Tinsley
993fc24dd3 Change inheritance of MediaPreviewV2Activity. 2022-12-16 12:03:01 -05:00
Greyson Parrelli
fddc6bcd5f Update maven endpoint for sqlcipher. 2022-12-16 12:02:42 -05:00
Cody Henthorne
558051086e Bump version to 6.5.5 2022-12-14 13:33:43 -05:00
Cody Henthorne
2c187bc55d Updated language translations. 2022-12-14 12:59:41 -05:00
Cody Henthorne
e947979169 Revert "Fix view flicker when switching between keyboard and attachment/emoji keyboards."
This reverts commit 1618141342.
2022-12-14 12:53:01 -05:00
Greyson Parrelli
08f1ddb212 Guard against potentially double-running a migration. 2022-12-14 11:15:23 -05:00
Cody Henthorne
4c318d8d82 Bump version to 6.5.4 2022-12-13 16:52:10 -05:00
Cody Henthorne
3e6ebfabb0 Updated language translations. 2022-12-13 16:41:01 -05:00
Alex Hart
55f4692d99 Add logging for response fields when an error happens. 2022-12-13 16:36:36 -05:00
Greyson Parrelli
ebe82cf3e6 Add back missing reaction triggers. 2022-12-13 16:36:36 -05:00
Greyson Parrelli
21a8434e4d Attempt to fix SQLite crash in migration. 2022-12-13 10:59:27 -05:00
Greyson Parrelli
4990778a97 Fix recipient remapping of sms/mms records. 2022-12-13 09:54:53 -05:00
Alex Hart
303e5c7996 Remove PayPal order complete sheet. 2022-12-12 16:05:54 -04:00
Alex Hart
599caee229 Add error handling to re-throw Stripe POST errors. 2022-12-12 15:59:34 -04:00
Cody Henthorne
e6f28c6cdd Bump version to 6.5.3 2022-12-12 12:48:59 -05:00
Cody Henthorne
fd3b0ee375 Updated language translations. 2022-12-12 12:41:54 -05:00
Greyson Parrelli
bd11ed9f17 Fix table drop order during backup import.
Fixes #12671
2022-12-12 11:54:05 -05:00
Alex Hart
a6a185004d Only brighten screen when flash is ON and camera is FRONT. 2022-12-12 12:53:25 -04:00
Alex Hart
3cc556d803 Fix issue with cache entry access. 2022-12-12 12:51:57 -04:00
Alex Hart
c3f9984346 Update error handling to include customized action when user cancels PayPal flow. 2022-12-12 11:54:56 -04:00
Cody Henthorne
10df4ee0d1 Add additional info when backup verification fails. 2022-12-12 10:49:55 -05:00
Greyson Parrelli
c03a183904 Fix transaction issue on backup restore. 2022-12-12 10:03:15 -05:00
Greyson Parrelli
a2893fbec7 Fix possible null column crash in V166 migration.
Fixes #12672
2022-12-12 09:30:49 -05:00
Alex Hart
19cbace33d Fix group search predicate causing crashing when creating group story. 2022-12-12 10:26:22 -04:00
Alex Hart
8a78481cca Bump version to 6.5.2 2022-12-09 14:34:51 -04:00
Alex Hart
e1fd254d15 Updated language translations. 2022-12-09 14:16:58 -04:00
Alex Hart
019219f1e1 Rotate paypal one-time flag. 2022-12-09 14:02:27 -04:00
Greyson Parrelli
ad3c04cb52 Fix ambiguous column in query. 2022-12-09 11:10:30 -05:00
Greyson Parrelli
61f9dc7498 Fix possible issue with reproducible builds.
- Needed to update apkdiff.py to ignore some new app-signing-related
  files.
- While I was in there, I cleaned up the script a lot to make it easier
  to read as well as extract files that didn't match.
- We also need to guarantee git hashes are the same length -- the script
  we were calling might provide hashes of different length depending on
  how you checked out the code.

Co-authored-by: inthewaves<26474149+inthewaves@users.noreply.github.com>
2022-12-09 08:53:17 -05:00
Alex Hart
4deb16a37a Bump version to 6.5.1 2022-12-08 14:20:33 -04:00
Alex Hart
4129151bd2 Updated language translations. 2022-12-08 14:17:26 -04:00
Cody Henthorne
10cf431537 Revert " Enable kotlin for libsignal-service project and convert SignalServiceDataMessage."
This reverts commit fc2b67aa0f.
2022-12-08 13:07:24 -05:00
Alex Hart
011dd2d973 Fix issue where gift receipt showed boost badge. 2022-12-08 13:45:44 -04:00
Alex Hart
c85c4c5020 Bump version to 6.5.0 2022-12-08 12:20:36 -04:00
Alex Hart
5f1439df00 Updated language translations. 2022-12-08 12:11:48 -04:00
Cody Henthorne
e76bec63a3 Remote ring small groups feature flag. 2022-12-08 12:07:02 -04:00
Cody Henthorne
fc2b67aa0f Enable kotlin for libsignal-service project and convert SignalServiceDataMessage. 2022-12-08 12:07:02 -04:00
Alex Hart
bcd0360dd0 Remove obselete unused dexOptions. 2022-12-08 12:07:02 -04:00
Cody Henthorne
04bf2cd0c2 Ignore decomissioned KBS enclaves when encountered during getToken. 2022-12-08 12:07:02 -04:00
Nicholas
aba51da932 Ensure view binding is valid after Media Preview animations. 2022-12-08 12:07:02 -04:00
Nicholas
f8520d83be Add null checks for FABs in conversation list.
Fixes #12651.
2022-12-08 12:07:02 -04:00
Greyson Parrelli
69003dfbe2 Convert IdentityTable to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
380b377ed8 Ensure we rotate storage id when applying hidden story state or username. 2022-12-08 12:07:02 -04:00
fm-sys
4c5db983e3 Make voice messages long-clickable.
Fixes #12658
2022-12-08 12:07:02 -04:00
Greyson Parrelli
48c887ac03 Add gradle test devices. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
f207a82d2f Show smaller quote chains within larger quote chains. 2022-12-08 12:07:02 -04:00
Cody Henthorne
56f6888d49 Update kotlin to 1.7.20 2022-12-08 12:07:02 -04:00
Alex Hart
66ece479f6 Update access modifiers. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
c1cc2b064c Convert SenderKeyTable to kotlin. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
98980b8192 Convert SenderKeySharedTable to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
79ec76f11f Update tooltip to behave better when content is at edge of screen. 2022-12-08 12:07:02 -04:00
Cody Henthorne
45a1c5c369 Fix mention crash with overlapping ranges. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
2dc41f319c Convert RemappedRecordTables to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
2cdb1b8300 Fix issue where story thumb could show as a chat image preview. 2022-12-08 12:07:02 -04:00
Alex Hart
e846b4e20a Add better onBack handling for donations webviews. 2022-12-08 12:07:02 -04:00
Alex Hart
961057f620 Implement PayPal confirm donation sheet. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
e686a09ce4 Convert GroupReceiptTable to kotlin. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
fc8cf2957f Convert DraftTable to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
0bef37bfc1 Add minimum amount error for boosts. 2022-12-07 13:03:02 -05:00
Cody Henthorne
1618141342 Fix view flicker when switching between keyboard and attachment/emoji keyboards. 2022-12-07 13:03:02 -05:00
Alex Hart
d7fb05f596 Fix integration tests. 2022-12-07 13:03:02 -05:00
Greyson Parrelli
2eb15cc8e3 Convert SearchTable to kotlin. 2022-12-07 13:03:02 -05:00
Alex Hart
424a0233c2 Implement refactor to utilize new donation configuration endpoint. 2022-12-07 13:03:02 -05:00
Alex Hart
40cf87307a Add improved handling for credit card errors. 2022-12-07 13:03:02 -05:00
Sgn-32
643206b946 SubmitDebugLogActivity progress dialog make-over.
Fixes #12656
2022-12-07 13:03:02 -05:00
Varsha
cc95041519 Fix navigation after sending payment from conversation. 2022-12-07 13:03:02 -05:00
Cody Henthorne
45b498f62f Remove unused resources. 2022-12-07 13:03:02 -05:00
Sgn-32
9e6d78ba5f Enable hyphenation on conversation settings buttons.
Closes #12609
2022-12-07 13:03:02 -05:00
Greyson Parrelli
95eba78d9c Improve constraints on thread and message tables. 2022-12-07 13:03:02 -05:00
Alex Hart
5d9f00b268 Fix issue when copying attachment data. 2022-12-07 13:03:02 -05:00
Alex Hart
6a01388e82 Ignore start/end clipping when directed to do so by transform properties. 2022-12-07 13:03:02 -05:00
Ehren Kret
2ef6f78d39 Remove some unused code in ConversationAdapter. 2022-12-07 13:03:02 -05:00
Alex Hart
a754c39599 Bump version to 6.4.2 2022-12-07 10:29:19 -04:00
Alex Hart
14622cd06c Updated language translations. 2022-12-07 10:29:02 -04:00
Cody Henthorne
3132cd1198 Drop group call rings for large groups. 2022-12-06 22:21:14 -05:00
Cody Henthorne
94c35d86e2 Update post translation qa tasks. 2022-12-06 15:11:20 -05:00
Cody Henthorne
3c2c6d782a Revert "Clear formatting when pasting text."
This reverts commit 77be721f5a.

If pasting an image will crash the application, does not handle pasting
via multiple other methods like quick suggestion or via a clipboard
manager like provided by Samsung via their keyboard.
2022-12-06 13:38:39 -05:00
Cody Henthorne
1764b21214 Fix crash when opening notification settings. 2022-12-06 13:11:22 -05:00
Greyson Parrelli
260e572071 Fix bug where disappearing timer was applied to sent group stories. 2022-12-05 17:36:57 -05:00
Greyson Parrelli
54251a27a8 Do not show stories for inactive groups. 2022-12-05 17:20:58 -05:00
Alex Hart
88a8430c31 Bump version to 6.4.1 2022-12-02 13:54:13 -04:00
Alex Hart
678b653873 Updated language translations. 2022-12-02 13:50:44 -04:00
Greyson Parrelli
21592ca5c0 Do not include archived messages in unread count. 2022-12-02 12:38:23 -05:00
gitstart
1bca2f06bd Pause voice memos when you open a video.
Fixes #11156.

Signed-off-by: Nicholas Tinsley <nicholas@signal.org>
2022-12-02 10:47:38 -05:00
Alex Hart
9f166105a6 Remove tinting when forwarding content. 2022-12-02 11:04:31 -04:00
Alex Hart
ea08b59e6b Fix error routing for credit cards. 2022-12-02 11:00:22 -04:00
Alex Hart
9aca0af22c Fix issue with poor sent video viewing behavior. 2022-12-02 10:43:31 -04:00
Alex Hart
591d8c3d1a Separate PayPal flags into one-time and recurring. 2022-12-02 09:13:58 -04:00
Nicholas
22b73494a7 Rename *Database androidTest classes to *Table. 2022-12-01 18:15:37 -05:00
Nicholas
9bb80077c6 Fix jumping from media to message in group converstations. 2022-12-01 18:15:09 -05:00
Cody Henthorne
646f41663f Fix in-chat payment message rendering with long note. 2022-12-01 10:20:27 -05:00
671 changed files with 16418 additions and 31151 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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() {

View File

@@ -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)
}
}

View File

@@ -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())

View File

@@ -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(

View File

@@ -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

View File

@@ -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
)
)

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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

View File

@@ -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))
}
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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!!)
}
}

View File

@@ -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)

View File

@@ -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()

View File

@@ -33,7 +33,7 @@ class SafetyNumberBottomSheetRepositoryTest {
testScheduler.triggerActions()
result.assertValueAt(1) { map ->
result.assertValueAt(0) { map ->
assertMatch(map, mapOf(SafetyNumberBucket.ContactsBucket to harness.others))
}
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -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"

View File

@@ -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();

View File

@@ -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();
}
}
}

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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
)
}

View File

@@ -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))

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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)
)

View File

@@ -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())
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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))
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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))

View File

@@ -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 ->

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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))
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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())
}

View File

@@ -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

View File

@@ -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))

View File

@@ -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)
}
}

View File

@@ -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(

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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()
}
}
}

View File

@@ -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)
}

View File

@@ -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
}
}
}
}

View File

@@ -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))
)
)
}
}
}

View File

@@ -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
)

View File

@@ -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) }
}
)

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

@@ -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) {

View File

@@ -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())
}
}

View File

@@ -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) {

View File

@@ -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()))
}
}

View File

@@ -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.
*/

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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

View File

@@ -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)

View File

@@ -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())
}

View File

@@ -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
}
}

View File

@@ -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())
}
}
}

View File

@@ -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)

View File

@@ -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))
}
}
}

View File

@@ -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(

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -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))

View File

@@ -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));

View File

@@ -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