mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-13 05:23:18 +01:00
Compare commits
253 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1621c060b5 | ||
|
|
b1c32476b0 | ||
|
|
ba96db2ae0 | ||
|
|
182a112cdd | ||
|
|
a45e26ab6b | ||
|
|
b6022be41f | ||
|
|
0801a0e329 | ||
|
|
89cbfd3299 | ||
|
|
5b2ca6a1d3 | ||
|
|
c5d7188dcb | ||
|
|
818eb81f87 | ||
|
|
510a295198 | ||
|
|
98fab95683 | ||
|
|
79d45bb497 | ||
|
|
fc3d77ed9a | ||
|
|
ee05cf87aa | ||
|
|
ae18aed15b | ||
|
|
a5aa079216 | ||
|
|
ae7a03bc8f | ||
|
|
6ed797c031 | ||
|
|
ef4015aec9 | ||
|
|
ffedc3fa7d | ||
|
|
20285e7e5b | ||
|
|
89e55a7133 | ||
|
|
11aa168a6b | ||
|
|
0fc6e642fe | ||
|
|
8e0553c849 | ||
|
|
75b4ffc16e | ||
|
|
643b07d564 | ||
|
|
637a44379c | ||
|
|
a2d42b0415 | ||
|
|
a76983ca0a | ||
|
|
22e79a045c | ||
|
|
061b87ead0 | ||
|
|
511abd67c6 | ||
|
|
1627d92009 | ||
|
|
2cb67f6ee3 | ||
|
|
13e0b8dec0 | ||
|
|
7626070c28 | ||
|
|
ca5140d3ec | ||
|
|
3694431503 | ||
|
|
cd1f0632fa | ||
|
|
1508b1d401 | ||
|
|
bf874e17e5 | ||
|
|
d2b8a17723 | ||
|
|
67cfdf101d | ||
|
|
125840e5fc | ||
|
|
f5ab4bec7a | ||
|
|
ef7d5d55cb | ||
|
|
1a9d785cbb | ||
|
|
cad0bab435 | ||
|
|
bdc3435fc1 | ||
|
|
f260633c9d | ||
|
|
8a00caabd7 | ||
|
|
b4fe5bdcc6 | ||
|
|
1f649057d6 | ||
|
|
41059a2b67 | ||
|
|
3d65a957f4 | ||
|
|
ff038e3ade | ||
|
|
44fa42fca4 | ||
|
|
73d8c74718 | ||
|
|
db4a0deccc | ||
|
|
8b23a409ef | ||
|
|
ec7e73bb7c | ||
|
|
321b85d5d0 | ||
|
|
98c9638bc4 | ||
|
|
de1c9f2581 | ||
|
|
1af6af5045 | ||
|
|
0121811195 | ||
|
|
18cf55b156 | ||
|
|
0d4e109c72 | ||
|
|
3e358da83a | ||
|
|
85453ca442 | ||
|
|
a5e5a73580 | ||
|
|
95f7b8d79f | ||
|
|
42d0d84ae0 | ||
|
|
686219d473 | ||
|
|
843ed24bbb | ||
|
|
e17c49505c | ||
|
|
473747ee03 | ||
|
|
9ea97aabbb | ||
|
|
811d79c873 | ||
|
|
018782e63d | ||
|
|
01070a9cc0 | ||
|
|
14aecc4684 | ||
|
|
8aea20f147 | ||
|
|
87f175a96b | ||
|
|
6b5117a609 | ||
|
|
0ab66f81be | ||
|
|
12ec0ca84c | ||
|
|
915d56ac15 | ||
|
|
ecc43f1dea | ||
|
|
d8a4678b8f | ||
|
|
306875478e | ||
|
|
2df303cde7 | ||
|
|
4309127b8c | ||
|
|
39155b55a0 | ||
|
|
02dc457636 | ||
|
|
732a6324d6 | ||
|
|
54614e67aa | ||
|
|
15362c04fb | ||
|
|
658de3b6e7 | ||
|
|
ab55fec6bd | ||
|
|
3a1f06f510 | ||
|
|
1ad0b0e6ae | ||
|
|
7ccc7ec856 | ||
|
|
f0ab919ca5 | ||
|
|
8a05626791 | ||
|
|
c0a468e42b | ||
|
|
8bee95eb02 | ||
|
|
dedb78e454 | ||
|
|
2c1f30db1d | ||
|
|
6d3319bfb1 | ||
|
|
e4b9832045 | ||
|
|
99aa4cbc98 | ||
|
|
1f952bd31e | ||
|
|
882bdcc726 | ||
|
|
b0f43535c6 | ||
|
|
16ae2c870f | ||
|
|
18bb876d1b | ||
|
|
dce8fde195 | ||
|
|
270ab34c6a | ||
|
|
aa872d29bc | ||
|
|
6315d4b96c | ||
|
|
0cb53f40f4 | ||
|
|
51c86cab10 | ||
|
|
1f860d41b5 | ||
|
|
573de99840 | ||
|
|
68e0a30c92 | ||
|
|
6fc9db0aff | ||
|
|
737d893c87 | ||
|
|
f06e1d9b98 | ||
|
|
4cff0a3369 | ||
|
|
cc64a922d7 | ||
|
|
e8c769bd1d | ||
|
|
deba07d6cb | ||
|
|
bacad359b2 | ||
|
|
a9d7417597 | ||
|
|
6b94fc82eb | ||
|
|
b9f060b442 | ||
|
|
ca24682366 | ||
|
|
5047fc54f2 | ||
|
|
48c115eba1 | ||
|
|
fd2677e8fe | ||
|
|
f6bd27eff9 | ||
|
|
ff41816fef | ||
|
|
1e6a17adc3 | ||
|
|
55aff18b1f | ||
|
|
5d6b3a8a75 | ||
|
|
31b98ec612 | ||
|
|
320bf45518 | ||
|
|
1893896254 | ||
|
|
19a95f479e | ||
|
|
5bcb7cece4 | ||
|
|
f4f5fe2789 | ||
|
|
e947212862 | ||
|
|
57f86b14fc | ||
|
|
e2dc7fb5bf | ||
|
|
6499ed4637 | ||
|
|
8c45600365 | ||
|
|
f8ef850fba | ||
|
|
151e2e5203 | ||
|
|
5dd3d8515f | ||
|
|
0f6c16c373 | ||
|
|
75bf3a7c7e | ||
|
|
48e47c9d92 | ||
|
|
3d45ab1b36 | ||
|
|
4d5d42157a | ||
|
|
a6dfee16e9 | ||
|
|
0e8550748d | ||
|
|
b82604953c | ||
|
|
100796b3b9 | ||
|
|
f5af964286 | ||
|
|
2836a6060d | ||
|
|
80e31051e6 | ||
|
|
1fb0573fec | ||
|
|
5ba04936b1 | ||
|
|
011f6e6cf4 | ||
|
|
ed3f992b83 | ||
|
|
782217a73d | ||
|
|
a37b89feaf | ||
|
|
e5b628b467 | ||
|
|
482a10de02 | ||
|
|
c4164b17a2 | ||
|
|
b8dc541fc5 | ||
|
|
2b6190bf34 | ||
|
|
2a70423a22 | ||
|
|
35c74573e7 | ||
|
|
c26c455b3c | ||
|
|
4e2e525509 | ||
|
|
ec83327eec | ||
|
|
bafb62f214 | ||
|
|
38f5e8b4eb | ||
|
|
9827deffd3 | ||
|
|
65105fd3cb | ||
|
|
5d604c4e55 | ||
|
|
22221222bd | ||
|
|
bad2f99968 | ||
|
|
392d582865 | ||
|
|
33dbf316a9 | ||
|
|
00a8565e91 | ||
|
|
0bac08dcc4 | ||
|
|
3b2dfb6ede | ||
|
|
997f6ef534 | ||
|
|
fb0b1af056 | ||
|
|
3037a33267 | ||
|
|
ff633ddd59 | ||
|
|
cae5dad5d8 | ||
|
|
1a03b8fc1d | ||
|
|
049ba6a706 | ||
|
|
f52364f75c | ||
|
|
87b699f3d8 | ||
|
|
f73b8a7fd2 | ||
|
|
8af8468f4d | ||
|
|
49270e677e | ||
|
|
09dd2583b9 | ||
|
|
dc22b27cd8 | ||
|
|
2a9eb1bae0 | ||
|
|
c06fb81490 | ||
|
|
af1b9579b4 | ||
|
|
7bbfc2d34c | ||
|
|
70355aa70e | ||
|
|
56c502c9bf | ||
|
|
a05793c882 | ||
|
|
53f60f5a4c | ||
|
|
43d969f6b5 | ||
|
|
a51bb8e23f | ||
|
|
5ceb3db0c4 | ||
|
|
9a65328c1b | ||
|
|
35eef0150d | ||
|
|
8511d3576f | ||
|
|
f6542440c7 | ||
|
|
35393fc331 | ||
|
|
f31e12572a | ||
|
|
cef7878b47 | ||
|
|
3574be913a | ||
|
|
b8cf0cc1be | ||
|
|
b0788f7307 | ||
|
|
cf9b91ebd4 | ||
|
|
1af15842cc | ||
|
|
17517cfc88 | ||
|
|
4615f246ac | ||
|
|
62ee60df82 | ||
|
|
4f3c545eda | ||
|
|
b92a41ab70 | ||
|
|
6673da0b04 | ||
|
|
102f9de06f | ||
|
|
614d6ce04b | ||
|
|
5bb48caafd | ||
|
|
6c7d837964 | ||
|
|
755ec672c0 | ||
|
|
186bd9db48 | ||
|
|
48a81da883 |
@@ -54,7 +54,7 @@ repositories {
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.10.0'
|
||||
artifact = 'com.google.protobuf:protoc:3.11.4'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
@@ -67,8 +67,8 @@ protobuf {
|
||||
}
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 946
|
||||
def canonicalVersionName = "5.25.8"
|
||||
def canonicalVersionCode = 966
|
||||
def canonicalVersionName = "5.27.7"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -80,16 +80,9 @@ def abiPostFix = ['universal' : 0,
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
def selectableVariants = [
|
||||
'internalProdFlipper',
|
||||
'internalProdPerf',
|
||||
'internalProdRelease',
|
||||
'internalStagingFlipper',
|
||||
'internalStagingPerf',
|
||||
'internalStagingRelease',
|
||||
'nightlyProdFlipper',
|
||||
'nightlyProdPerf',
|
||||
'nightlyProdRelease',
|
||||
'nightlyStagingPerf',
|
||||
'playProdDebug',
|
||||
'playProdFlipper',
|
||||
'playProdPerf',
|
||||
@@ -160,7 +153,7 @@ android {
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
|
||||
buildConfigField "String", "CDSH_CODE_HASH", "\"ec31a51880d19a5e9e0fed404740c1a3ff53a553125564b745acce475f0fded8\""
|
||||
buildConfigField "String", "CDSH_CODE_HASH", "\"ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2\""
|
||||
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
|
||||
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
|
||||
@@ -171,7 +164,7 @@ android {
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
||||
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44,49,33,41}"
|
||||
buildConfigField "int[]", "MOBILE_COIN_BLACKLIST", "new int[]{98,963,53,850,7}"
|
||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||
|
||||
@@ -299,14 +292,6 @@ android {
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
|
||||
}
|
||||
|
||||
internal {
|
||||
dimension 'distribution'
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"internal\""
|
||||
}
|
||||
|
||||
nightly {
|
||||
dimension 'distribution'
|
||||
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
|
||||
@@ -465,7 +450,6 @@ dependencies {
|
||||
implementation project(':image-editor')
|
||||
implementation project(':donations')
|
||||
|
||||
implementation libs.signal.zkgroup.android
|
||||
implementation libs.signal.client.android
|
||||
implementation libs.google.protobuf.javalite
|
||||
|
||||
@@ -533,6 +517,7 @@ dependencies {
|
||||
|
||||
flipperImplementation libs.facebook.flipper
|
||||
flipperImplementation libs.facebook.soloader
|
||||
flipperImplementation libs.square.leakcanary
|
||||
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
@@ -613,7 +598,8 @@ def getCurrentGitTag() {
|
||||
def output = stdout.toString().trim()
|
||||
|
||||
if (output != null && output.size() > 0) {
|
||||
return output.split('\n')[0];
|
||||
def tags = output.split('\n').toList()
|
||||
return tags.stream().filter(t -> t.contains('nightly')).findFirst().orElse(tags.get(0))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
@@ -13,6 +11,7 @@ import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.UUID
|
||||
|
||||
@@ -23,7 +22,7 @@ class RecipientDatabaseTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
recipientDatabase = SignalDatabase.recipients
|
||||
ensureDbEmpty()
|
||||
}
|
||||
|
||||
@@ -37,7 +36,7 @@ class RecipientDatabaseTest {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireUuid())
|
||||
assertEquals(ACI_A, recipient.requireAci())
|
||||
assertFalse(recipient.hasE164())
|
||||
}
|
||||
|
||||
@@ -47,7 +46,7 @@ class RecipientDatabaseTest {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, false)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireUuid())
|
||||
assertEquals(ACI_A, recipient.requireAci())
|
||||
assertFalse(recipient.hasE164())
|
||||
}
|
||||
|
||||
@@ -58,7 +57,7 @@ class RecipientDatabaseTest {
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(E164_A, recipient.requireE164())
|
||||
assertFalse(recipient.hasUuid())
|
||||
assertFalse(recipient.hasAci())
|
||||
}
|
||||
|
||||
/** If all you have is an E164, you can just store that, regardless of trust level. */
|
||||
@@ -68,7 +67,7 @@ class RecipientDatabaseTest {
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(E164_A, recipient.requireE164())
|
||||
assertFalse(recipient.hasUuid())
|
||||
assertFalse(recipient.hasAci())
|
||||
}
|
||||
|
||||
/** With high trust, you can associate an ACI-e164 pair. */
|
||||
@@ -77,7 +76,7 @@ class RecipientDatabaseTest {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireUuid())
|
||||
assertEquals(ACI_A, recipient.requireAci())
|
||||
assertEquals(E164_A, recipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -87,7 +86,7 @@ class RecipientDatabaseTest {
|
||||
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertEquals(ACI_A, recipient.requireUuid())
|
||||
assertEquals(ACI_A, recipient.requireAci())
|
||||
assertFalse(recipient.hasE164())
|
||||
}
|
||||
|
||||
@@ -98,26 +97,26 @@ class RecipientDatabaseTest {
|
||||
/** With high trust, you can associate an e164 with an existing ACI. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_highTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromUuid(ACI_A)
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** With low trust, you cannot associate an ACI-e164 pair, and therefore cannot store the e164. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_lowTrust() {
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromUuid(ACI_A)
|
||||
val existingId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
}
|
||||
|
||||
@@ -130,7 +129,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -143,7 +142,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -160,7 +159,7 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -173,12 +172,12 @@ class RecipientDatabaseTest {
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(E164_A, existingRecipient.requireE164())
|
||||
assertFalse(existingRecipient.hasUuid())
|
||||
assertFalse(existingRecipient.hasAci())
|
||||
}
|
||||
|
||||
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. But high trust lets us take the e164 from the current holder. */
|
||||
@@ -190,11 +189,11 @@ class RecipientDatabaseTest {
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_B, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_B, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(ACI_A, existingRecipient.requireUuid())
|
||||
assertEquals(ACI_A, existingRecipient.requireAci())
|
||||
assertFalse(existingRecipient.hasE164())
|
||||
}
|
||||
|
||||
@@ -207,11 +206,11 @@ class RecipientDatabaseTest {
|
||||
assertNotEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_B, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_B, retrievedRecipient.requireAci())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingRecipient = Recipient.resolved(existingId)
|
||||
assertEquals(ACI_A, existingRecipient.requireUuid())
|
||||
assertEquals(ACI_A, existingRecipient.requireAci())
|
||||
assertEquals(E164_A, existingRecipient.requireE164())
|
||||
}
|
||||
|
||||
@@ -228,21 +227,21 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
}
|
||||
|
||||
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() {
|
||||
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromUuid(ACI_A)
|
||||
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
@@ -252,19 +251,19 @@ class RecipientDatabaseTest {
|
||||
/** Low trust means you can’t merge. If you’re retrieving a user from the table with this data, prefer the ACI one. */
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_lowTrust() {
|
||||
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromUuid(ACI_A)
|
||||
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
|
||||
assertEquals(existingAciId, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertFalse(retrievedRecipient.hasE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(existingE164Id)
|
||||
assertEquals(E164_A, existingE164Recipient.requireE164())
|
||||
assertFalse(existingE164Recipient.hasUuid())
|
||||
assertFalse(existingE164Recipient.hasAci())
|
||||
}
|
||||
|
||||
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
|
||||
@@ -277,11 +276,11 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId1, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingRecipient2 = Recipient.resolved(existingId2)
|
||||
assertEquals(ACI_B, existingRecipient2.requireUuid())
|
||||
assertEquals(ACI_B, existingRecipient2.requireAci())
|
||||
assertFalse(existingRecipient2.hasE164())
|
||||
}
|
||||
|
||||
@@ -295,14 +294,36 @@ class RecipientDatabaseTest {
|
||||
assertEquals(existingId1, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_B, retrievedRecipient.requireE164())
|
||||
|
||||
val existingRecipient2 = Recipient.resolved(existingId2)
|
||||
assertEquals(ACI_B, existingRecipient2.requireUuid())
|
||||
assertEquals(ACI_B, existingRecipient2.requireAci())
|
||||
assertEquals(E164_A, existingRecipient2.requireE164())
|
||||
}
|
||||
|
||||
/**
|
||||
* Another high trust case that results in a merge. Nothing strictly new here, but this case is called out because it’s a merge but *also* an E164 change,
|
||||
* which clients may need to know for UX purposes.
|
||||
*/
|
||||
@Test
|
||||
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange_highTrust() {
|
||||
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
|
||||
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
|
||||
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
assertEquals(existingId1, retrievedId)
|
||||
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
|
||||
|
||||
val recipientWithId2 = Recipient.resolved(existingId2)
|
||||
assertEquals(retrievedId, recipientWithId2.id)
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// Misc
|
||||
// ==============================================================
|
||||
@@ -327,18 +348,18 @@ class RecipientDatabaseTest {
|
||||
@Test
|
||||
fun createByUuidSanityCheck() {
|
||||
// GIVEN one recipient
|
||||
val recipientId: RecipientId = recipientDatabase.getOrInsertFromUuid(ACI_A)
|
||||
val recipientId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
|
||||
// WHEN I retrieve one by UUID
|
||||
val possible: Optional<RecipientId> = recipientDatabase.getByUuid(ACI_A)
|
||||
val possible: Optional<RecipientId> = recipientDatabase.getByAci(ACI_A)
|
||||
|
||||
// THEN I get it back, and it has the properties I expect
|
||||
assertTrue(possible.isPresent)
|
||||
assertEquals(recipientId, possible.get())
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
assertTrue(recipient.uuid.isPresent)
|
||||
assertEquals(ACI_A, recipient.uuid.get())
|
||||
assertTrue(recipient.aci.isPresent)
|
||||
assertEquals(ACI_A, recipient.aci.get())
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
@@ -346,19 +367,16 @@ class RecipientDatabaseTest {
|
||||
recipientDatabase.getAndPossiblyMerge(null, null, true)
|
||||
}
|
||||
|
||||
private val context: Application
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
|
||||
|
||||
private fun ensureDbEmpty() {
|
||||
DatabaseFactory.getInstance(context).rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||
assertTrue(cursor.moveToFirst())
|
||||
assertEquals(0, cursor.getLong(0))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ACI_A = UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e")
|
||||
val ACI_B = UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed")
|
||||
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
val E164_A = "+12221234567"
|
||||
val E164_B = "+13331234567"
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.database
|
||||
import android.app.Application
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
@@ -16,7 +15,9 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.signal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -27,6 +28,7 @@ import org.whispersystems.libsignal.IdentityKey
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress
|
||||
import org.whispersystems.libsignal.state.SessionRecord
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.UUID
|
||||
|
||||
@@ -42,18 +44,20 @@ class RecipientDatabaseTest_merges {
|
||||
private lateinit var mmsDatabase: MessageDatabase
|
||||
private lateinit var sessionDatabase: SessionDatabase
|
||||
private lateinit var mentionDatabase: MentionDatabase
|
||||
private lateinit var reactionDatabase: ReactionDatabase
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
identityDatabase = DatabaseFactory.getIdentityDatabase(context)
|
||||
groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context)
|
||||
groupDatabase = DatabaseFactory.getGroupDatabase(context)
|
||||
threadDatabase = DatabaseFactory.getThreadDatabase(context)
|
||||
smsDatabase = DatabaseFactory.getSmsDatabase(context)
|
||||
mmsDatabase = DatabaseFactory.getMmsDatabase(context)
|
||||
sessionDatabase = DatabaseFactory.getSessionDatabase(context)
|
||||
mentionDatabase = DatabaseFactory.getMentionDatabase(context)
|
||||
recipientDatabase = SignalDatabase.recipients
|
||||
identityDatabase = SignalDatabase.identities
|
||||
groupReceiptDatabase = SignalDatabase.groupReceipts
|
||||
groupDatabase = SignalDatabase.groups
|
||||
threadDatabase = SignalDatabase.threads
|
||||
smsDatabase = SignalDatabase.sms
|
||||
mmsDatabase = SignalDatabase.mms
|
||||
sessionDatabase = SignalDatabase.sessions
|
||||
mentionDatabase = SignalDatabase.mentions
|
||||
reactionDatabase = SignalDatabase.reactions
|
||||
|
||||
ensureDbEmpty()
|
||||
}
|
||||
@@ -62,7 +66,7 @@ class RecipientDatabaseTest_merges {
|
||||
@Test
|
||||
fun getAndPossiblyMerge_general() {
|
||||
// Setup
|
||||
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromUuid(ACI_A)
|
||||
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
|
||||
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
|
||||
|
||||
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
|
||||
@@ -90,6 +94,9 @@ class RecipientDatabaseTest_merges {
|
||||
|
||||
sessionDatabase.store(SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
|
||||
|
||||
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
|
||||
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
|
||||
|
||||
// Merge
|
||||
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
|
||||
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
|
||||
@@ -97,7 +104,7 @@ class RecipientDatabaseTest_merges {
|
||||
|
||||
// Recipient validation
|
||||
val retrievedRecipient = Recipient.resolved(retrievedId)
|
||||
assertEquals(ACI_A, retrievedRecipient.requireUuid())
|
||||
assertEquals(ACI_A, retrievedRecipient.requireAci())
|
||||
assertEquals(E164_A, retrievedRecipient.requireE164())
|
||||
|
||||
val existingE164Recipient = Recipient.resolved(recipientIdE164)
|
||||
@@ -154,13 +161,23 @@ class RecipientDatabaseTest_merges {
|
||||
|
||||
// Session validation
|
||||
assertNotNull(sessionDatabase.load(SignalProtocolAddress(ACI_A.toString(), 1)))
|
||||
|
||||
// Reaction validation
|
||||
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
|
||||
val reactionsMms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(mmsId1, true))
|
||||
|
||||
assertEquals(1, reactionsSms.size)
|
||||
assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0])
|
||||
|
||||
assertEquals(1, reactionsMms.size)
|
||||
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
|
||||
}
|
||||
|
||||
private val context: Application
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
|
||||
|
||||
private fun ensureDbEmpty() {
|
||||
DatabaseFactory.getInstance(context).rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
|
||||
assertTrue(cursor.moveToFirst())
|
||||
assertEquals(0, cursor.getLong(0))
|
||||
}
|
||||
@@ -194,8 +211,7 @@ class RecipientDatabaseTest_merges {
|
||||
}
|
||||
|
||||
private fun getMention(messageId: Long): MentionModel {
|
||||
val db: SQLiteDatabase = DatabaseFactory.getInstance(context).rawDatabase
|
||||
db.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME}").use { cursor ->
|
||||
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor ->
|
||||
cursor.moveToFirst()
|
||||
return MentionModel(
|
||||
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)),
|
||||
@@ -211,8 +227,8 @@ class RecipientDatabaseTest_merges {
|
||||
)
|
||||
|
||||
companion object {
|
||||
val ACI_A = UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e")
|
||||
val ACI_B = UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed")
|
||||
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
val E164_A = "+12221234567"
|
||||
val E164_B = "+13331234567"
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter;
|
||||
|
||||
public class FlipperApplicationContext extends ApplicationContext {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
SoLoader.init(this, false);
|
||||
|
||||
FlipperClient client = AndroidFlipperClient.getInstance(this);
|
||||
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new DatabasesFlipperPlugin(new FlipperSqlCipherAdapter(this)));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
|
||||
client.start();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import com.facebook.flipper.android.AndroidFlipperClient
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
|
||||
import com.facebook.soloader.SoLoader
|
||||
import leakcanary.LeakCanary
|
||||
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter
|
||||
import shark.AndroidReferenceMatchers
|
||||
|
||||
class FlipperApplicationContext : ApplicationContext() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
SoLoader.init(this, false)
|
||||
|
||||
val client = AndroidFlipperClient.getInstance(this)
|
||||
client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
|
||||
client.addPlugin(DatabasesFlipperPlugin(FlipperSqlCipherAdapter(this)))
|
||||
client.addPlugin(SharedPreferencesFlipperPlugin(this))
|
||||
client.start()
|
||||
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.service.media.MediaBrowserService\$ServiceBinder",
|
||||
fieldName = "this\$0"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "androidx.media.MediaBrowserServiceCompat\$MediaBrowserServiceImplApi26\$MediaBrowserServiceApi26",
|
||||
fieldName = "mBase"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.MediaBrowserCompat",
|
||||
fieldName = "mImpl"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||
fieldName = "mToken"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||
fieldName = "mImpl"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService",
|
||||
fieldName = "mApplication"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.service.GenericForegroundService\$LocalBinder",
|
||||
fieldName = "this\$0"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.contacts.ContactsSyncAdapter",
|
||||
fieldName = "mContext"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -43,14 +42,11 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
||||
@Override
|
||||
public List<Descriptor> getDatabases() {
|
||||
try {
|
||||
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
|
||||
databaseHelperField.setAccessible(true);
|
||||
|
||||
SignalDatabase mainOpenHelper = Objects.requireNonNull((SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext())));
|
||||
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
|
||||
SignalDatabase metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper mainOpenHelper = Objects.requireNonNull(SignalDatabase.getInstance());
|
||||
SignalDatabaseOpenHelper keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
|
||||
SignalDatabaseOpenHelper metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
|
||||
|
||||
return Arrays.asList(new Descriptor(mainOpenHelper),
|
||||
new Descriptor(keyValueOpenHelper),
|
||||
@@ -253,9 +249,9 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
|
||||
}
|
||||
|
||||
static class Descriptor implements DatabaseDescriptor {
|
||||
private final SignalDatabase sqlCipherOpenHelper;
|
||||
private final SignalDatabaseOpenHelper sqlCipherOpenHelper;
|
||||
|
||||
Descriptor(@NonNull SignalDatabase sqlCipherOpenHelper) {
|
||||
Descriptor(@NonNull SignalDatabaseOpenHelper sqlCipherOpenHelper) {
|
||||
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/core_red_shade"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -399,7 +399,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.ConversationSettings"
|
||||
android:windowSoftInputMode="stateAlwaysHidden">
|
||||
</activity>
|
||||
@@ -770,22 +770,6 @@
|
||||
|
||||
</provider>
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Conversation"
|
||||
android:authorities="${applicationId}.database.conversation"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Attachment"
|
||||
android:authorities="${applicationId}.database.attachment"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$Sticker"
|
||||
android:authorities="${applicationId}.database.sticker"
|
||||
android:exported="false" />
|
||||
|
||||
<provider android:name=".database.DatabaseContentProviders$StickerPack"
|
||||
android:authorities="${applicationId}.database.stickerpack"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver android:name=".service.BootReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
|
||||
@@ -37,10 +37,11 @@ import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
@@ -51,9 +52,11 @@ import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
@@ -61,8 +64,8 @@ import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
@@ -70,13 +73,13 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.SubscriberIdKeepAliveListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
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.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -124,7 +127,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
super.onCreate();
|
||||
|
||||
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
|
||||
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load())
|
||||
.addBlocking("sqlcipher-init", () -> {
|
||||
SqlCipherLibraryLoader.load();
|
||||
SignalDatabase.init(this,
|
||||
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
|
||||
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
|
||||
})
|
||||
.addBlocking("logging", () -> {
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
@@ -160,7 +168,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(this::cleanAvatarStorage)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
.addNonBlocking(this::initializeGcmCheck)
|
||||
.addNonBlocking(this::initializeFcmCheck)
|
||||
.addNonBlocking(this::initializeSignedPreKeyCheck)
|
||||
.addNonBlocking(this::initializePeriodicTasks)
|
||||
.addNonBlocking(this::initializeCircumvention)
|
||||
@@ -172,12 +180,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addNonBlocking(this::ensureProfileUploaded)
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> DatabaseFactory.getMessageLogDatabase(this).trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -193,6 +202,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getFrameRateTracker().start();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
ApplicationDependencies.getDeadlockDetector().start();
|
||||
SubscriptionKeepAliveJob.launchSubscriberIdKeepAliveJobIfNecessary();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
@@ -284,7 +294,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
private void initializeFirstEverAppLaunch() {
|
||||
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
|
||||
if (!SQLCipherOpenHelper.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
|
||||
if (!SignalDatabase.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
|
||||
Log.i(TAG, "First ever app launch!");
|
||||
AppInitialization.onFirstEverAppLaunch(this);
|
||||
}
|
||||
@@ -300,11 +310,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeGcmCheck() {
|
||||
if (TextSecurePreferences.isPushRegistered(this)) {
|
||||
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6);
|
||||
private void initializeFcmCheck() {
|
||||
if (SignalStore.account().isRegistered()) {
|
||||
long nextSetTime = SignalStore.account().getFcmTokenLastSetTime() + TimeUnit.HOURS.toMillis(6);
|
||||
|
||||
if (TextSecurePreferences.getFcmToken(this) == null || nextSetTime <= System.currentTimeMillis()) {
|
||||
if (SignalStore.account().getFcmToken() == null || nextSetTime <= System.currentTimeMillis()) {
|
||||
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
|
||||
}
|
||||
}
|
||||
@@ -334,7 +344,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
LocalBackupListener.schedule(this);
|
||||
RotateSenderCertificateListener.schedule(this);
|
||||
MessageProcessReceiver.startOrUpdateAlarm(this);
|
||||
SubscriberIdKeepAliveListener.schedule(this);
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
@@ -351,7 +360,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
@WorkerThread
|
||||
private void initializeCircumvention() {
|
||||
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
|
||||
if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) {
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(ApplicationContext.this);
|
||||
} catch (Throwable t) {
|
||||
@@ -360,6 +369,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureProfileUploaded() {
|
||||
if (SignalStore.account().isRegistered() && !SignalStore.registrationValues().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
|
||||
Log.w(TAG, "User has a profile, but has not uploaded one. Uploading now.");
|
||||
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
|
||||
}
|
||||
}
|
||||
|
||||
private void executePendingContactSync() {
|
||||
if (TextSecurePreferences.needsFullContactSync(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
|
||||
@@ -390,7 +406,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
@WorkerThread
|
||||
private void initializeCleanup() {
|
||||
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
|
||||
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
@@ -65,7 +65,7 @@ public final class BlockUnblockDialog {
|
||||
Resources resources = context.getResources();
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
|
||||
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
|
||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
|
||||
@@ -104,7 +104,7 @@ public final class BlockUnblockDialog {
|
||||
Resources resources = context.getResources();
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
|
||||
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||
|
||||
@@ -26,7 +26,6 @@ import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -572,7 +571,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
|
||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return UsernameUtil.fetchUuidForUsername(requireContext(), contact.getNumber());
|
||||
return UsernameUtil.fetchAciForUsername(requireContext(), contact.getNumber());
|
||||
}, uuid -> {
|
||||
loadingDialog.dismiss();
|
||||
if (uuid.isPresent()) {
|
||||
|
||||
@@ -191,7 +191,6 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext()));
|
||||
|
||||
TextSecurePreferences.setMultiDevice(DeviceActivity.this, true);
|
||||
TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false);
|
||||
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode);
|
||||
|
||||
return SUCCESS;
|
||||
|
||||
@@ -35,6 +35,7 @@ public final class GroupMembersDialog {
|
||||
.show();
|
||||
|
||||
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
|
||||
memberListView.initializeAdapter(fragmentActivity);
|
||||
|
||||
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
|
||||
LiveData<List<GroupMemberEntry.FullMember>> fullMembers = liveGroup.getFullMembers();
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -255,7 +255,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
|
||||
|
||||
if (recipient.getContactUri() != null) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setHasSentInvite(recipient.getId());
|
||||
SignalDatabase.recipients().setHasSentInvite(recipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -568,26 +568,11 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
if (item == 0) {
|
||||
viewPagerListener.onPageSelected(0);
|
||||
}
|
||||
|
||||
cursor.registerContentObserver(new ContentObserver(new Handler(getMainLooper())) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
onMediaChange();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mediaNotAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
private void onMediaChange() {
|
||||
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
|
||||
|
||||
if (adapter != null) {
|
||||
adapter.checkMedia(mediaPager.getCurrentItem());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
|
||||
|
||||
@@ -615,7 +600,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (adapter != null) {
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
if (item.recipient != null) item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
|
||||
if (item != null && item.recipient != null) {
|
||||
item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
|
||||
}
|
||||
|
||||
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
|
||||
initializeActionBar();
|
||||
}
|
||||
@@ -628,7 +616,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
|
||||
if (adapter != null) {
|
||||
MediaItem item = adapter.getMediaItemFor(position);
|
||||
if (item.recipient != null) item.recipient.live().removeObservers(MediaPreviewActivity.this);
|
||||
if (item != null && item.recipient != null) {
|
||||
item.recipient.live().removeObservers(MediaPreviewActivity.this);
|
||||
}
|
||||
|
||||
adapter.pause(position);
|
||||
}
|
||||
@@ -678,7 +668,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
public @Nullable MediaItem getMediaItemFor(int position) {
|
||||
return new MediaItem(null, null, null, uri, mediaType, -1, true);
|
||||
}
|
||||
|
||||
@@ -701,11 +691,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
public boolean hasFragmentFor(int position) {
|
||||
return mediaPreviewFragment != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMedia(int currentItem) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
|
||||
@@ -789,8 +774,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
super.destroyItem(container, position, object);
|
||||
}
|
||||
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
cursor.moveToPosition(getCursorPosition(position));
|
||||
public @Nullable MediaItem getMediaItemFor(int position) {
|
||||
int cursorPosition = getCursorPosition(position);
|
||||
|
||||
if (cursor.isClosed() || cursorPosition < 0) {
|
||||
Log.w(TAG, "Invalid cursor state! Closed: " + cursor.isClosed() + " Position: " + cursorPosition);
|
||||
return null;
|
||||
}
|
||||
|
||||
cursor.moveToPosition(cursorPosition);
|
||||
|
||||
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
|
||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||
@@ -824,14 +816,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
return mediaFragments.containsKey(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkMedia(int position) {
|
||||
MediaPreviewFragment fragment = mediaFragments.get(position);
|
||||
if (fragment != null) {
|
||||
fragment.checkMediaStillAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
private int getCursorPosition(int position) {
|
||||
if (leftIsRecent) return position;
|
||||
else return cursor.getCount() - 1 - position;
|
||||
@@ -866,10 +850,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
||||
}
|
||||
|
||||
interface MediaItemAdapter {
|
||||
MediaItem getMediaItemFor(int position);
|
||||
@Nullable MediaItem getMediaItemFor(int position);
|
||||
void pause(int position);
|
||||
@Nullable View getPlaybackControls(int position);
|
||||
boolean hasFragmentFor(int position);
|
||||
void checkMedia(int currentItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@@ -67,7 +67,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
} else {
|
||||
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
|
||||
|
||||
if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) {
|
||||
if (SignalStore.account().isRegistered() && NetworkConstraint.isMet(this)) {
|
||||
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
|
||||
|
||||
AlertDialog progress = SimpleProgressDialog.show(this);
|
||||
@@ -75,7 +75,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Recipient resolved = Recipient.external(this, number);
|
||||
|
||||
if (!resolved.isRegistered() || !resolved.hasUuid()) {
|
||||
if (!resolved.isRegistered() || !resolved.hasAci()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||
try {
|
||||
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
|
||||
@@ -103,7 +103,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||
long existingThread = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
|
||||
Intent intent = ConversationIntents.createBuilder(this, recipient.getId(), existingThread)
|
||||
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
|
||||
.withDataUri(getIntent().getData())
|
||||
|
||||
@@ -60,7 +60,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
protected final void onCreate(Bundle savedInstanceState) {
|
||||
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
|
||||
AppStartup.getInstance().onCriticalRenderEventStart();
|
||||
this.networkAccess = new SignalServiceNetworkAccess(this);
|
||||
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
|
||||
onPreCreate();
|
||||
|
||||
final boolean locked = KeyCachingService.isLocked(this);
|
||||
@@ -84,7 +84,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (networkAccess.isCensored(this)) {
|
||||
if (networkAccess.isCensored()) {
|
||||
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Rfc5724Uri;
|
||||
|
||||
@@ -48,7 +48,7 @@ public class SmsSendtoActivity extends Activity {
|
||||
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Recipient recipient = Recipient.external(this, destination.getDestination());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId());
|
||||
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
|
||||
|
||||
nextIntent = ConversationIntents.createBuilder(this, recipient.getId(), threadId)
|
||||
.withDraftText(destination.getBody())
|
||||
|
||||
@@ -56,7 +56,6 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.OneShotPreDrawListener;
|
||||
@@ -64,6 +63,8 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
@@ -165,7 +166,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
|
||||
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
|
||||
extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
|
||||
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, TextSecurePreferences.getLocalNumber(this));
|
||||
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, Recipient.self().requireE164());
|
||||
extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false));
|
||||
|
||||
scanFragment.setScanListener(this);
|
||||
@@ -322,23 +323,26 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
||||
//noinspection WrongThread
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
|
||||
if (FeatureFlags.verifyV2() && resolved.getAci().isPresent()) {
|
||||
Log.i(TAG, "Using UUID (version 2).");
|
||||
version = 2;
|
||||
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
|
||||
remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
|
||||
localId = Recipient.self().requireAci().toByteArray();
|
||||
remoteId = resolved.requireAci().toByteArray();
|
||||
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
|
||||
Log.i(TAG, "Using E164 (version 1).");
|
||||
version = 1;
|
||||
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
|
||||
localId = Recipient.self().requireE164().getBytes();
|
||||
remoteId = resolved.requireE164().getBytes();
|
||||
} else {
|
||||
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getUuid().isPresent(), resolved.getE164().isPresent()));
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
|
||||
.setOnDismissListener(dialog -> requireActivity().finish())
|
||||
.show();
|
||||
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getAci().isPresent(), resolved.getE164().isPresent()));
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
|
||||
.setOnDismissListener(dialog -> {
|
||||
requireActivity().finish();
|
||||
dialog.dismiss();
|
||||
})
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -484,6 +484,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
delayedFinish();
|
||||
}
|
||||
|
||||
private void handleGlare(@NonNull Recipient recipient) {
|
||||
Log.i(TAG, "handleGlare: " + recipient.getId());
|
||||
|
||||
callScreen.setStatus("");
|
||||
}
|
||||
|
||||
private void handleCallRinging() {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_ringing));
|
||||
}
|
||||
@@ -629,6 +635,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
handleCallRinging(); break;
|
||||
case CALL_DISCONNECTED:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
|
||||
case CALL_DISCONNECTED_GLARE:
|
||||
handleGlare(event.getRecipient()); break;
|
||||
case CALL_ACCEPTED_ELSEWHERE:
|
||||
handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
|
||||
case CALL_DECLINED_ELSEWHERE:
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
|
||||
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
|
||||
import org.thoughtcrime.securesms.media.MediaInput;
|
||||
@@ -100,7 +100,7 @@ public final class AudioWaveForm {
|
||||
|
||||
if (attachment instanceof DatabaseAttachment) {
|
||||
try {
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
|
||||
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.avatar
|
||||
|
||||
import android.os.Bundle
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
/**
|
||||
* Utility class which encapsulates reading and writing Avatar objects to and from Bundles.
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.avatar
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
@@ -33,7 +33,7 @@ object AvatarPickerStorage {
|
||||
@JvmStatic
|
||||
fun cleanOrphans(context: Context) {
|
||||
val avatarFiles = FileStorage.getAllFiles(context, DIRECTORY, FILENAME_BASE)
|
||||
val database = DatabaseFactory.getAvatarPickerDatabase(context)
|
||||
val database = SignalDatabase.avatarPicker
|
||||
val photoAvatars = database
|
||||
.getAllAvatars()
|
||||
.filterIsInstance<Avatar.Photo>()
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.AvatarBundler
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
|
||||
|
||||
@@ -44,7 +44,7 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
|
||||
val inputStream = BlobProvider.getInstance().getStream(applicationContext, editedImageUri)
|
||||
val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream)
|
||||
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
|
||||
val database = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val database = SignalDatabase.avatarPicker
|
||||
val newPhoto = photo.copy(uri = onDiskUri, size = size)
|
||||
|
||||
database.update(newPhoto)
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
|
||||
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper
|
||||
@@ -70,11 +70,11 @@ class AvatarPickerRepository(context: Context) {
|
||||
}
|
||||
|
||||
fun getPersistedAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
|
||||
DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForSelf()
|
||||
SignalDatabase.avatarPicker.getAvatarsForSelf()
|
||||
}
|
||||
|
||||
fun getPersistedAvatarsForGroup(groupId: GroupId): Single<List<Avatar>> = Single.fromCallable {
|
||||
DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForGroup(groupId)
|
||||
SignalDatabase.avatarPicker.getAvatarsForGroup(groupId)
|
||||
}
|
||||
|
||||
fun getDefaultAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
|
||||
@@ -97,7 +97,7 @@ class AvatarPickerRepository(context: Context) {
|
||||
|
||||
fun persistAvatarForSelf(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val avatarDatabase = SignalDatabase.avatarPicker
|
||||
val savedAvatar = avatarDatabase.saveAvatarForSelf(avatar)
|
||||
avatarDatabase.markUsage(savedAvatar)
|
||||
onPersisted(savedAvatar)
|
||||
@@ -106,7 +106,7 @@ class AvatarPickerRepository(context: Context) {
|
||||
|
||||
fun persistAvatarForGroup(avatar: Avatar, groupId: GroupId, onPersisted: (Avatar) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val avatarDatabase = SignalDatabase.avatarPicker
|
||||
val savedAvatar = avatarDatabase.saveAvatarForGroup(avatar, groupId)
|
||||
avatarDatabase.markUsage(savedAvatar)
|
||||
onPersisted(savedAvatar)
|
||||
@@ -180,7 +180,7 @@ class AvatarPickerRepository(context: Context) {
|
||||
fun delete(avatar: Avatar, onDelete: () -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
if (avatar.databaseId is Avatar.DatabaseId.Saved) {
|
||||
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
|
||||
val avatarDatabase = SignalDatabase.avatarPicker
|
||||
avatarDatabase.deleteAvatar(avatar)
|
||||
}
|
||||
onDelete()
|
||||
|
||||
@@ -43,7 +43,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
@@ -9,9 +9,9 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
class BadgeImageView @JvmOverloads constructor(
|
||||
@@ -25,34 +25,70 @@ class BadgeImageView @JvmOverloads constructor(
|
||||
context.obtainStyledAttributes(attrs, R.styleable.BadgeImageView).use {
|
||||
badgeSize = it.getInt(R.styleable.BadgeImageView_badge_size, 0)
|
||||
}
|
||||
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
override fun setOnClickListener(l: OnClickListener?) {
|
||||
val wasClickable = isClickable
|
||||
super.setOnClickListener(l)
|
||||
this.isClickable = wasClickable
|
||||
}
|
||||
|
||||
fun setBadgeFromRecipient(recipient: Recipient?) {
|
||||
getGlideRequests()?.let {
|
||||
setBadgeFromRecipient(recipient, it)
|
||||
} ?: clearDrawable()
|
||||
}
|
||||
|
||||
fun setBadgeFromRecipient(recipient: Recipient?, glideRequests: GlideRequests) {
|
||||
if (recipient == null || recipient.badges.isEmpty()) {
|
||||
setBadge(null)
|
||||
setBadge(null, glideRequests)
|
||||
} else if (recipient.isSelf) {
|
||||
val badge = recipient.featuredBadge
|
||||
if (badge == null || !badge.visible || badge.isExpired()) {
|
||||
setBadge(null, glideRequests)
|
||||
} else {
|
||||
setBadge(badge, glideRequests)
|
||||
}
|
||||
} else {
|
||||
setBadge(recipient.badges[0])
|
||||
setBadge(recipient.featuredBadge, glideRequests)
|
||||
}
|
||||
}
|
||||
|
||||
fun setBadge(badge: Badge?) {
|
||||
visible = badge != null
|
||||
getGlideRequests()?.let {
|
||||
setBadge(badge, it)
|
||||
} ?: clearDrawable()
|
||||
}
|
||||
|
||||
try {
|
||||
if (badge != null) {
|
||||
GlideApp
|
||||
.with(this)
|
||||
.load(badge)
|
||||
.downsample(DownsampleStrategy.NONE)
|
||||
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context)))
|
||||
.into(this)
|
||||
} else {
|
||||
GlideApp
|
||||
.with(this)
|
||||
.clear(this)
|
||||
}
|
||||
fun setBadge(badge: Badge?, glideRequests: GlideRequests) {
|
||||
if (badge != null) {
|
||||
glideRequests
|
||||
.load(badge)
|
||||
.downsample(DownsampleStrategy.NONE)
|
||||
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context)))
|
||||
.into(this)
|
||||
|
||||
isClickable = true
|
||||
} else {
|
||||
glideRequests
|
||||
.clear(this)
|
||||
clearDrawable()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearDrawable() {
|
||||
setImageDrawable(null)
|
||||
isClickable = false
|
||||
}
|
||||
|
||||
private fun getGlideRequests(): GlideRequests? {
|
||||
return try {
|
||||
GlideApp.with(this)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Do nothing. Activity was destroyed.
|
||||
// View not attached to an activity or activity destroyed
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,38 @@ import android.content.Context
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.ProfileUtil
|
||||
|
||||
class BadgeRepository(context: Context) {
|
||||
|
||||
private val context = context.applicationContext
|
||||
|
||||
fun setVisibilityForAllBadges(displayBadgesOnProfile: Boolean): Completable = Completable.fromAction {
|
||||
val badges = Recipient.self().badges.map { it.copy(visible = displayBadgesOnProfile) }
|
||||
ProfileUtil.uploadProfileWithBadges(context, badges)
|
||||
fun setVisibilityForAllBadges(
|
||||
displayBadgesOnProfile: Boolean,
|
||||
selfBadges: List<Badge> = Recipient.self().badges
|
||||
): Completable = Completable.fromAction {
|
||||
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
|
||||
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
|
||||
|
||||
ProfileUtil.uploadProfileWithBadges(context, badges)
|
||||
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile)
|
||||
recipientDatabase.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
|
||||
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
recipientDatabase.setBadges(Recipient.self().id, badges)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
|
||||
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
|
||||
val badges = Recipient.self().badges
|
||||
val reOrderedBadges = listOf(featuredBadge) + (badges - featuredBadge)
|
||||
val reOrderedBadges = listOf(featuredBadge.copy(visible = true)) + (badges.filterNot { it.id == featuredBadge.id })
|
||||
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
|
||||
|
||||
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
|
||||
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import com.google.android.flexbox.AlignItems
|
||||
import com.google.android.flexbox.FlexDirection
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
|
||||
import org.thoughtcrime.securesms.util.ScreenDensity
|
||||
import org.whispersystems.libsignal.util.Pair
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
@@ -35,8 +36,11 @@ object Badges {
|
||||
}
|
||||
.forEach { customPref(it) }
|
||||
|
||||
val perRow = context.resources.getInteger(R.integer.badge_columns)
|
||||
val empties = (perRow - (badges.size % perRow)) % perRow
|
||||
val badgeSize = DimensionUnit.DP.toPixels(88f)
|
||||
val windowWidth = context.resources.displayMetrics.widthPixels
|
||||
val perRow = (windowWidth / badgeSize).toInt()
|
||||
|
||||
val empties = ((perRow - (badges.size % perRow)) % perRow)
|
||||
repeat(empties) {
|
||||
customPref(Badge.EmptyModel())
|
||||
}
|
||||
@@ -59,14 +63,13 @@ object Badges {
|
||||
}
|
||||
|
||||
private fun getBestBadgeImageUriForDevice(serviceBadge: SignalServiceProfile.Badge): Pair<Uri, String> {
|
||||
val bestDensity = ScreenDensity.getBestDensityBucketForDevice()
|
||||
return when (bestDensity) {
|
||||
return when (ScreenDensity.getBestDensityBucketForDevice()) {
|
||||
"ldpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[0]), "ldpi")
|
||||
"mdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[1]), "mdpi")
|
||||
"hdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[2]), "hdpi")
|
||||
"xxhdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[4]), "xxhdpi")
|
||||
"xxxhdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[5]), "xxxhdpi")
|
||||
else -> Pair(getBadgeImageUri(serviceBadge.sprites6[3]), "xdpi")
|
||||
else -> Pair(getBadgeImageUri(serviceBadge.sprites6[3]), "xhdpi")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +77,34 @@ object Badges {
|
||||
return Timestamp(bigDecimal.toLong() * 1000).time
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun fromDatabaseBadge(badge: BadgeList.Badge): Badge {
|
||||
return Badge(
|
||||
badge.id,
|
||||
fromCode(badge.category),
|
||||
badge.name,
|
||||
badge.description,
|
||||
Uri.parse(badge.imageUrl),
|
||||
badge.imageDensity,
|
||||
badge.expiration,
|
||||
badge.visible
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun toDatabaseBadge(badge: Badge): BadgeList.Badge {
|
||||
return BadgeList.Badge.newBuilder()
|
||||
.setId(badge.id)
|
||||
.setCategory(badge.category.code)
|
||||
.setDescription(badge.description)
|
||||
.setExpiration(badge.expirationTimestamp)
|
||||
.setVisible(badge.visible)
|
||||
.setName(badge.name)
|
||||
.setImageUrl(badge.imageUrl.toString())
|
||||
.setImageDensity(badge.imageDensity)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun fromServiceBadge(serviceBadge: SignalServiceProfile.Badge): Badge {
|
||||
val uriAndDensity: Pair<Uri, String> = getBestBadgeImageUriForDevice(serviceBadge)
|
||||
|
||||
@@ -45,9 +45,9 @@ class BadgeSpriteTransformation(
|
||||
SMALL(
|
||||
"small",
|
||||
mapOf(
|
||||
Density.LDPI to FrameSet(Frame(124, 1, 13, 13), Frame(145, 31, 13, 13)),
|
||||
Density.LDPI to FrameSet(Frame(124, 1, 12, 12), Frame(145, 31, 12, 12)),
|
||||
Density.MDPI to FrameSet(Frame(163, 1, 16, 16), Frame(189, 39, 16, 16)),
|
||||
Density.HDPI to FrameSet(Frame(244, 1, 25, 25), Frame(283, 58, 25, 25)),
|
||||
Density.HDPI to FrameSet(Frame(244, 1, 24, 24), Frame(283, 58, 24, 24)),
|
||||
Density.XHDPI to FrameSet(Frame(323, 1, 32, 32), Frame(373, 75, 32, 32)),
|
||||
Density.XXHDPI to FrameSet(Frame(483, 1, 48, 48), Frame(557, 111, 48, 48)),
|
||||
Density.XXXHDPI to FrameSet(Frame(643, 1, 64, 64), Frame(741, 147, 64, 64))
|
||||
@@ -56,9 +56,9 @@ class BadgeSpriteTransformation(
|
||||
MEDIUM(
|
||||
"medium",
|
||||
mapOf(
|
||||
Density.LDPI to FrameSet(Frame(124, 16, 19, 19), Frame(160, 31, 19, 19)),
|
||||
Density.LDPI to FrameSet(Frame(124, 16, 18, 18), Frame(160, 31, 18, 18)),
|
||||
Density.MDPI to FrameSet(Frame(163, 19, 24, 24), Frame(207, 39, 24, 24)),
|
||||
Density.HDPI to FrameSet(Frame(244, 28, 37, 37), Frame(310, 58, 37, 37)),
|
||||
Density.HDPI to FrameSet(Frame(244, 28, 36, 36), Frame(310, 58, 36, 36)),
|
||||
Density.XHDPI to FrameSet(Frame(323, 35, 48, 48), Frame(407, 75, 48, 48)),
|
||||
Density.XXHDPI to FrameSet(Frame(483, 51, 72, 72), Frame(607, 111, 72, 72)),
|
||||
Density.XXXHDPI to FrameSet(Frame(643, 67, 96, 96), Frame(807, 147, 96, 96))
|
||||
@@ -67,20 +67,42 @@ class BadgeSpriteTransformation(
|
||||
LARGE(
|
||||
"large",
|
||||
mapOf(
|
||||
Density.LDPI to FrameSet(Frame(145, 1, 28, 28), Frame(124, 46, 28, 28)),
|
||||
Density.LDPI to FrameSet(Frame(145, 1, 27, 27), Frame(124, 46, 27, 27)),
|
||||
Density.MDPI to FrameSet(Frame(189, 1, 36, 36), Frame(163, 57, 36, 36)),
|
||||
Density.HDPI to FrameSet(Frame(283, 1, 55, 55), Frame(244, 85, 55, 55)),
|
||||
Density.HDPI to FrameSet(Frame(283, 1, 54, 54), Frame(244, 85, 54, 54)),
|
||||
Density.XHDPI to FrameSet(Frame(373, 1, 72, 72), Frame(323, 109, 72, 72)),
|
||||
Density.XXHDPI to FrameSet(Frame(557, 1, 108, 108), Frame(483, 161, 108, 108)),
|
||||
Density.XXXHDPI to FrameSet(Frame(741, 1, 144, 144), Frame(643, 213, 144, 144))
|
||||
)
|
||||
),
|
||||
BADGE_64(
|
||||
"badge_64",
|
||||
mapOf(
|
||||
Density.LDPI to FrameSet(Frame(124, 73, 48, 48), Frame(124, 73, 48, 48)),
|
||||
Density.MDPI to FrameSet(Frame(163, 97, 64, 64), Frame(163, 97, 64, 64)),
|
||||
Density.HDPI to FrameSet(Frame(244, 145, 96, 96), Frame(244, 145, 96, 96)),
|
||||
Density.XHDPI to FrameSet(Frame(323, 193, 128, 128), Frame(323, 193, 128, 128)),
|
||||
Density.XXHDPI to FrameSet(Frame(483, 289, 192, 192), Frame(483, 289, 192, 192)),
|
||||
Density.XXXHDPI to FrameSet(Frame(643, 385, 256, 256), Frame(643, 385, 256, 256))
|
||||
)
|
||||
),
|
||||
BADGE_112(
|
||||
"badge_112",
|
||||
mapOf(
|
||||
Density.LDPI to FrameSet(Frame(181, 1, 84, 84), Frame(181, 1, 84, 84)),
|
||||
Density.MDPI to FrameSet(Frame(233, 1, 112, 112), Frame(233, 1, 112, 112)),
|
||||
Density.HDPI to FrameSet(Frame(349, 1, 168, 168), Frame(349, 1, 168, 168)),
|
||||
Density.XHDPI to FrameSet(Frame(457, 1, 224, 224), Frame(457, 1, 224, 224)),
|
||||
Density.XXHDPI to FrameSet(Frame(681, 1, 336, 336), Frame(681, 1, 336, 336)),
|
||||
Density.XXXHDPI to FrameSet(Frame(905, 1, 448, 448), Frame(905, 1, 448, 448))
|
||||
)
|
||||
),
|
||||
XLARGE(
|
||||
"xlarge",
|
||||
mapOf(
|
||||
Density.LDPI to FrameSet(Frame(1, 1, 121, 121), Frame(1, 1, 121, 121)),
|
||||
Density.LDPI to FrameSet(Frame(1, 1, 120, 120), Frame(1, 1, 120, 120)),
|
||||
Density.MDPI to FrameSet(Frame(1, 1, 160, 160), Frame(1, 1, 160, 160)),
|
||||
Density.HDPI to FrameSet(Frame(1, 1, 241, 241), Frame(1, 1, 241, 241)),
|
||||
Density.HDPI to FrameSet(Frame(1, 1, 240, 240), Frame(1, 1, 240, 240)),
|
||||
Density.XHDPI to FrameSet(Frame(1, 1, 320, 320), Frame(1, 1, 320, 320)),
|
||||
Density.XXHDPI to FrameSet(Frame(1, 1, 480, 480), Frame(1, 1, 480, 480)),
|
||||
Density.XXXHDPI to FrameSet(Frame(1, 1, 640, 640), Frame(1, 1, 640, 640))
|
||||
@@ -94,6 +116,8 @@ class BadgeSpriteTransformation(
|
||||
1 -> MEDIUM
|
||||
2 -> LARGE
|
||||
3 -> XLARGE
|
||||
4 -> BADGE_64
|
||||
5 -> BADGE_112
|
||||
else -> LARGE
|
||||
}
|
||||
}
|
||||
@@ -123,7 +147,7 @@ class BadgeSpriteTransformation(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VERSION = 1
|
||||
private const val VERSION = 3
|
||||
|
||||
private fun getDensity(density: String): Density {
|
||||
return Density.values().first { it.density == density }
|
||||
|
||||
@@ -36,7 +36,8 @@ data class Badge(
|
||||
val visible: Boolean,
|
||||
) : Parcelable, Key {
|
||||
|
||||
fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis()
|
||||
fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis() && expirationTimestamp > 0
|
||||
fun isBoost(): Boolean = id == BOOST_BADGE_ID
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(id.toByteArray(Key.CHARSET))
|
||||
@@ -128,7 +129,7 @@ data class Badge(
|
||||
.downsample(DownsampleStrategy.NONE)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transform(
|
||||
BadgeSpriteTransformation(BadgeSpriteTransformation.Size.XLARGE, model.badge.imageDensity, ThemeUtil.isDarkTheme(context)),
|
||||
BadgeSpriteTransformation(BadgeSpriteTransformation.Size.BADGE_64, model.badge.imageDensity, ThemeUtil.isDarkTheme(context)),
|
||||
)
|
||||
.into(badge)
|
||||
|
||||
@@ -159,6 +160,8 @@ data class Badge(
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val BOOST_BADGE_ID = "BOOST"
|
||||
|
||||
private val SELECTION_CHANGED = Any()
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
|
||||
|
||||
@@ -12,13 +12,13 @@ data class LargeBadge(
|
||||
val badge: Badge
|
||||
) {
|
||||
|
||||
class Model(val largeBadge: LargeBadge, val shortName: String) : MappingModel<Model> {
|
||||
class Model(val largeBadge: LargeBadge, val shortName: String, val maxLines: Int) : MappingModel<Model> {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return newItem.largeBadge.badge.id == largeBadge.badge.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return newItem.largeBadge == largeBadge && newItem.shortName == shortName
|
||||
return newItem.largeBadge == largeBadge && newItem.shortName == shortName && newItem.maxLines == maxLines
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ data class LargeBadge(
|
||||
|
||||
name.text = model.largeBadge.badge.name
|
||||
description.text = model.largeBadge.badge.resolveDescription(model.shortName)
|
||||
description.setLines(model.maxLines)
|
||||
description.maxLines = model.maxLines
|
||||
description.minLines = model.maxLines
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package org.thoughtcrime.securesms.badges.self.expired
|
||||
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
|
||||
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.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
|
||||
/**
|
||||
* Bottom sheet displaying a fading badge with a notice and action for becoming a subscriber again.
|
||||
@@ -23,18 +27,32 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
}
|
||||
|
||||
private fun getConfiguration(): DSLConfiguration {
|
||||
val badge = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments()).badge
|
||||
val badge: Badge = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments()).badge
|
||||
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
|
||||
|
||||
return configure {
|
||||
customPref(ExpiredBadge.Model(badge))
|
||||
|
||||
sectionHeaderPref(R.string.ExpiredBadgeBottomSheetDialogFragment__your_badge_has_expired)
|
||||
sectionHeaderPref(
|
||||
DSLSettingsText.from(
|
||||
if (badge.isBoost()) {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__your_badge_has_expired
|
||||
} else {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__subscription_cancelled
|
||||
},
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(4f).toInt())
|
||||
|
||||
noPadTextPref(
|
||||
DSLSettingsText.from(
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_s_badge_has_expired, badge.name),
|
||||
if (badge.isBoost()) {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired)
|
||||
} else {
|
||||
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer, badge.name)
|
||||
},
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
@@ -43,7 +61,15 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
|
||||
noPadTextPref(
|
||||
DSLSettingsText.from(
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting,
|
||||
if (badge.isBoost()) {
|
||||
if (isLikelyASustainer) {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_reactivate
|
||||
} else {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting_technology
|
||||
}
|
||||
} else {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can
|
||||
},
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
@@ -51,10 +77,24 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
space(DimensionUnit.DP.toPixels(92f).toInt())
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.ExpiredBadgeBottomSheetDialogFragment__become_a_subscriber),
|
||||
text = DSLSettingsText.from(
|
||||
if (badge.isBoost()) {
|
||||
if (isLikelyASustainer) {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__add_a_boost
|
||||
} else {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__become_a_sustainer
|
||||
}
|
||||
} else {
|
||||
R.string.ExpiredBadgeBottomSheetDialogFragment__renew_subscription
|
||||
}
|
||||
),
|
||||
onClick = {
|
||||
dismiss()
|
||||
findNavController().navigate(R.id.action_directly_to_subscribe)
|
||||
if (isLikelyASustainer) {
|
||||
requireActivity().startActivity(AppSettingsActivity.boost(requireContext()))
|
||||
} else {
|
||||
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -66,4 +106,15 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun show(badge: Badge, fragmentManager: FragmentManager) {
|
||||
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge).build()
|
||||
val fragment = ExpiredBadgeBottomSheetDialogFragment()
|
||||
fragment.arguments = args.toBundle()
|
||||
|
||||
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.badges.self.none
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
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.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
|
||||
class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
private val viewModel: BecomeASustainerViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
BecomeASustainerViewModel.Factory(SubscriptionsRepository(ApplicationDependencies.getDonationsService()))
|
||||
}
|
||||
)
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
BadgePreview.register(adapter)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(getConfiguration(it).toMappingModelList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: BecomeASustainerState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(BadgePreview.Model(badge = state.badge))
|
||||
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.BecomeASustainerFragment__get_badges,
|
||||
DSLSettingsText.CenterModifier,
|
||||
DSLSettingsText.Title2BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.BecomeASustainerFragment__signal_is_a_non_profit,
|
||||
DSLSettingsText.CenterModifier
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(77f).toInt())
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(
|
||||
R.string.BecomeASustainerMegaphone__become_a_sustainer
|
||||
),
|
||||
onClick = {
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
}
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager) {
|
||||
BecomeASustainerFragment().show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.thoughtcrime.securesms.badges.self.none
|
||||
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
|
||||
data class BecomeASustainerState(
|
||||
val badge: Badge? = null
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.badges.self.none
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class BecomeASustainerViewModel(subscriptionsRepository: SubscriptionsRepository) : ViewModel() {
|
||||
|
||||
private val store = Store(BecomeASustainerState())
|
||||
|
||||
val state: LiveData<BecomeASustainerState> = store.stateLiveData
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
init {
|
||||
disposables += subscriptionsRepository.getSubscriptions().subscribeBy(
|
||||
onError = { Log.w(TAG, "Could not load subscriptions.") },
|
||||
onSuccess = { subscriptions ->
|
||||
store.update {
|
||||
it.copy(badge = subscriptions.firstOrNull()?.badge)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BecomeASustainerViewModel::class.java)
|
||||
}
|
||||
|
||||
class Factory(private val subscriptionsRepository: SubscriptionsRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(BecomeASustainerViewModel(subscriptionsRepository))!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,10 +68,11 @@ class BadgesOverviewFragment : DSLSettingsFragment(
|
||||
fadedBadgeId = state.fadedBadgeId
|
||||
)
|
||||
|
||||
switchPref(
|
||||
asyncSwitchPref(
|
||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile),
|
||||
isChecked = state.displayBadgesOnProfile,
|
||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges,
|
||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges && state.hasInternet,
|
||||
isProcessing = state.stage == BadgesOverviewState.Stage.UPDATING_BADGE_DISPLAY_STATE,
|
||||
onClick = {
|
||||
viewModel.setDisplayBadgesOnProfile(!state.displayBadgesOnProfile)
|
||||
}
|
||||
@@ -80,7 +81,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__featured_badge),
|
||||
summary = state.featuredBadge?.name?.let { DSLSettingsText.from(it) },
|
||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges,
|
||||
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges && state.hasInternet,
|
||||
onClick = {
|
||||
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ data class BadgesOverviewState(
|
||||
val allUnlockedBadges: List<Badge> = listOf(),
|
||||
val featuredBadge: Badge? = null,
|
||||
val displayBadgesOnProfile: Boolean = false,
|
||||
val fadedBadgeId: String? = null
|
||||
val fadedBadgeId: String? = null,
|
||||
val hasInternet: Boolean = false
|
||||
) {
|
||||
|
||||
val hasUnexpiredBadges = allUnlockedBadges.any { it.expirationTimestamp > System.currentTimeMillis() }
|
||||
@@ -15,6 +16,6 @@ data class BadgesOverviewState(
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
UPDATING
|
||||
UPDATING_BADGE_DISPLAY_STATE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
|
||||
@@ -37,26 +38,38 @@ class BadgesOverviewViewModel(
|
||||
state.copy(
|
||||
stage = if (state.stage == BadgesOverviewState.Stage.INIT) BadgesOverviewState.Stage.READY else state.stage,
|
||||
allUnlockedBadges = recipient.badges,
|
||||
displayBadgesOnProfile = recipient.badges.firstOrNull()?.visible == true,
|
||||
displayBadgesOnProfile = SignalStore.donationsValues().getDisplayBadgesOnProfile(),
|
||||
featuredBadge = recipient.featuredBadge
|
||||
)
|
||||
}
|
||||
|
||||
disposables += InternetConnectionObserver.observe()
|
||||
.distinctUntilChanged()
|
||||
.subscribeBy { isConnected ->
|
||||
store.update { it.copy(hasInternet = isConnected) }
|
||||
}
|
||||
|
||||
disposables += Single.zip(
|
||||
subscriptionsRepository.getActiveSubscription(),
|
||||
subscriptionsRepository.getSubscriptions(SignalStore.donationsValues().getSubscriptionCurrency())
|
||||
subscriptionsRepository.getSubscriptions()
|
||||
) { active, all ->
|
||||
if (!active.isActive && active.activeSubscription?.willCancelAtPeriodEnd() == true) {
|
||||
Optional.fromNullable<String>(all.firstOrNull { it.level == active.activeSubscription?.level }?.badge?.id)
|
||||
} else {
|
||||
Optional.absent()
|
||||
}
|
||||
}.subscribeBy { badgeId ->
|
||||
store.update { it.copy(fadedBadgeId = badgeId.orNull()) }
|
||||
}
|
||||
}.subscribeBy(
|
||||
onSuccess = { badgeId ->
|
||||
store.update { it.copy(fadedBadgeId = badgeId.orNull()) }
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Could not retrieve data from server", throwable)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun setDisplayBadgesOnProfile(displayBadgesOnProfile: Boolean) {
|
||||
store.update { it.copy(stage = BadgesOverviewState.Stage.UPDATING_BADGE_DISPLAY_STATE) }
|
||||
disposables += badgeRepository.setVisibilityForAllBadges(displayBadgesOnProfile)
|
||||
.subscribe(
|
||||
{
|
||||
@@ -82,4 +95,8 @@ class BadgesOverviewViewModel(
|
||||
return requireNotNull(modelClass.cast(BadgesOverviewViewModel(badgeRepository, subscriptionsRepository)))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BadgesOverviewViewModel::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package org.thoughtcrime.securesms.badges.view
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
@@ -15,11 +18,18 @@ import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.LargeBadge
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
|
||||
class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
@@ -27,6 +37,13 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
||||
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
|
||||
private val textWidth: Float
|
||||
get() = (resources.displayMetrics.widthPixels - ViewUtil.dpToPx(64)).toFloat()
|
||||
private val textBounds: Rect = Rect()
|
||||
private val textPaint: Paint = Paint().apply {
|
||||
textSize = ViewUtil.spToPx(16f).toFloat()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.view_badge_bottom_sheet_dialog_fragment, container, false)
|
||||
}
|
||||
@@ -37,11 +54,31 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
||||
val pager: ViewPager2 = view.findViewById(R.id.pager)
|
||||
val tabs: TabLayout = view.findViewById(R.id.tab_layout)
|
||||
val action: MaterialButton = view.findViewById(R.id.action)
|
||||
val noSupport: View = view.findViewById(R.id.no_support)
|
||||
|
||||
if (getRecipientId() == Recipient.self().id) {
|
||||
action.visible = false
|
||||
}
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (PlayServicesUtil.getPlayServicesStatus(requireContext()) != PlayServicesUtil.PlayServicesStatus.SUCCESS) {
|
||||
noSupport.visible = true
|
||||
action.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_open_20)
|
||||
action.setText(R.string.preferences__donate_to_signal)
|
||||
action.setOnClickListener {
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
|
||||
}
|
||||
} else if (
|
||||
FeatureFlags.donorBadges() &&
|
||||
Recipient.self().badges.none { it.category == Badge.Category.Donor && !it.isBoost() && !it.isExpired() }
|
||||
) {
|
||||
action.setOnClickListener {
|
||||
startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||
}
|
||||
} else {
|
||||
action.visible = false
|
||||
}
|
||||
|
||||
val adapter = MappingAdapter()
|
||||
|
||||
LargeBadge.register(adapter)
|
||||
@@ -70,9 +107,17 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
||||
|
||||
tabs.visible = state.allBadgesVisibleOnProfile.size > 1
|
||||
|
||||
var maxLines = 3
|
||||
state.allBadgesVisibleOnProfile.forEach { badge ->
|
||||
val text = badge.resolveDescription(state.recipient.getShortDisplayName(requireContext()))
|
||||
textPaint.getTextBounds(text, 0, text.length, textBounds)
|
||||
val estimatedLines = ceil(textBounds.width().toFloat() / textWidth).toInt()
|
||||
maxLines = max(maxLines, estimatedLines)
|
||||
}
|
||||
|
||||
adapter.submitList(
|
||||
state.allBadgesVisibleOnProfile.map {
|
||||
LargeBadge.Model(LargeBadge(it), state.recipient.getShortDisplayName(requireContext()))
|
||||
LargeBadge.Model(LargeBadge(it), state.recipient.getShortDisplayName(requireContext()), maxLines + 1)
|
||||
}
|
||||
) {
|
||||
val stateSelectedIndex = state.allBadgesVisibleOnProfile.indexOf(state.selectedBadge)
|
||||
@@ -98,6 +143,10 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
|
||||
recipientId: RecipientId,
|
||||
startBadge: Badge? = null
|
||||
) {
|
||||
if (!FeatureFlags.displayDonorBadges() && recipientId != Recipient.self().id) {
|
||||
return
|
||||
}
|
||||
|
||||
ViewBadgeBottomSheetDialogFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putParcelable(ARG_START_BADGE, startBadge)
|
||||
|
||||
@@ -7,8 +7,8 @@ import androidx.core.util.Consumer;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -32,7 +32,7 @@ class BlockedUsersRepository {
|
||||
|
||||
void getBlocked(@NonNull Consumer<List<Recipient>> blockedUsers) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
RecipientDatabase db = DatabaseFactory.getRecipientDatabase(context);
|
||||
RecipientDatabase db = SignalDatabase.recipients();
|
||||
try (RecipientDatabase.RecipientReader reader = db.readerForBlocked(db.getBlocked())) {
|
||||
int count = reader.getCount();
|
||||
if (count == 0) {
|
||||
|
||||
@@ -119,6 +119,7 @@ public final class AudioView extends FrameLayout {
|
||||
|
||||
lottieDirection = REVERSE;
|
||||
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
||||
this.playPauseButton.setOnLongClickListener(v -> performLongClick());
|
||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||
|
||||
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));
|
||||
|
||||
@@ -225,6 +225,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
blurred = shouldBlur;
|
||||
|
||||
GlideRequest<Drawable> request = requestManager.load(photo.contactPhoto)
|
||||
.dontAnimate()
|
||||
.fallback(fallbackContactPhotoDrawable)
|
||||
.error(fallbackContactPhotoDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
@@ -290,6 +291,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(avatarBytes)
|
||||
.dontAnimate()
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
|
||||
@@ -28,7 +28,7 @@ import com.airbnb.lottie.model.KeyPath;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
@@ -360,8 +360,11 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
long id = messageRecord.getId();
|
||||
boolean mms = messageRecord.isMms();
|
||||
|
||||
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
|
||||
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
|
||||
if (mms) {
|
||||
SignalDatabase.mms().markExpireStarted(id);
|
||||
} else {
|
||||
SignalDatabase.sms().markExpireStarted(id);
|
||||
}
|
||||
|
||||
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
@@ -26,6 +27,9 @@ public class ConversationTypingView extends ConstraintLayout {
|
||||
private AvatarImageView avatar1;
|
||||
private AvatarImageView avatar2;
|
||||
private AvatarImageView avatar3;
|
||||
private BadgeImageView badge1;
|
||||
private BadgeImageView badge2;
|
||||
private BadgeImageView badge3;
|
||||
private View bubble;
|
||||
private TypingIndicatorView indicator;
|
||||
private TextView typistCount;
|
||||
@@ -41,6 +45,9 @@ public class ConversationTypingView extends ConstraintLayout {
|
||||
avatar1 = findViewById(R.id.typing_avatar_1);
|
||||
avatar2 = findViewById(R.id.typing_avatar_2);
|
||||
avatar3 = findViewById(R.id.typing_avatar_3);
|
||||
badge1 = findViewById(R.id.typing_badge_1);
|
||||
badge2 = findViewById(R.id.typing_badge_2);
|
||||
badge3 = findViewById(R.id.typing_badge_3);
|
||||
typistCount = findViewById(R.id.typing_count);
|
||||
bubble = findViewById(R.id.typing_bubble);
|
||||
indicator = findViewById(R.id.typing_indicator);
|
||||
@@ -55,6 +62,9 @@ public class ConversationTypingView extends ConstraintLayout {
|
||||
avatar1.setVisibility(GONE);
|
||||
avatar2.setVisibility(GONE);
|
||||
avatar3.setVisibility(GONE);
|
||||
badge1.setVisibility(GONE);
|
||||
badge2.setVisibility(GONE);
|
||||
badge3.setVisibility(GONE);
|
||||
typistCount.setVisibility(GONE);
|
||||
|
||||
if (isGroupThread) {
|
||||
@@ -75,15 +85,21 @@ public class ConversationTypingView extends ConstraintLayout {
|
||||
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
|
||||
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
|
||||
avatar1.setVisibility(VISIBLE);
|
||||
badge1.setBadgeFromRecipient(typists.get(0), glideRequests);
|
||||
badge1.setVisibility(VISIBLE);
|
||||
|
||||
if (typists.size() > 1) {
|
||||
avatar2.setAvatar(glideRequests, typists.get(1), false);
|
||||
avatar2.setVisibility(VISIBLE);
|
||||
badge2.setBadgeFromRecipient(typists.get(1), glideRequests);
|
||||
badge2.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (typists.size() == 3) {
|
||||
avatar3.setAvatar(glideRequests, typists.get(2), false);
|
||||
avatar3.setVisibility(VISIBLE);
|
||||
badge3.setBadgeFromRecipient(typists.get(2), glideRequests);
|
||||
badge3.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
if (typists.size() > 3) {
|
||||
|
||||
@@ -17,19 +17,11 @@ public class DeliveryStatusView extends FrameLayout {
|
||||
|
||||
private static final String TAG = Log.tag(DeliveryStatusView.class);
|
||||
|
||||
private static final RotateAnimation ROTATION_ANIMATION = new RotateAnimation(0, 360f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
static {
|
||||
ROTATION_ANIMATION.setInterpolator(new LinearInterpolator());
|
||||
ROTATION_ANIMATION.setDuration(1500);
|
||||
ROTATION_ANIMATION.setRepeatCount(Animation.INFINITE);
|
||||
}
|
||||
|
||||
private final ImageView pendingIndicator;
|
||||
private final ImageView sentIndicator;
|
||||
private final ImageView deliveredIndicator;
|
||||
private final ImageView readIndicator;
|
||||
private final RotateAnimation rotationAnimation;
|
||||
private final ImageView pendingIndicator;
|
||||
private final ImageView sentIndicator;
|
||||
private final ImageView deliveredIndicator;
|
||||
private final ImageView readIndicator;
|
||||
|
||||
public DeliveryStatusView(Context context) {
|
||||
this(context, null);
|
||||
@@ -44,10 +36,17 @@ public class DeliveryStatusView extends FrameLayout {
|
||||
|
||||
inflate(context, R.layout.delivery_status_view, this);
|
||||
|
||||
this.deliveredIndicator = findViewById(R.id.delivered_indicator);
|
||||
this.sentIndicator = findViewById(R.id.sent_indicator);
|
||||
this.pendingIndicator = findViewById(R.id.pending_indicator);
|
||||
this.readIndicator = findViewById(R.id.read_indicator);
|
||||
this.deliveredIndicator = findViewById(R.id.delivered_indicator);
|
||||
this.sentIndicator = findViewById(R.id.sent_indicator);
|
||||
this.pendingIndicator = findViewById(R.id.pending_indicator);
|
||||
this.readIndicator = findViewById(R.id.read_indicator);
|
||||
|
||||
rotationAnimation = new RotateAnimation(0, 360f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
rotationAnimation.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimation.setDuration(1500);
|
||||
rotationAnimation.setRepeatCount(Animation.INFINITE);
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DeliveryStatusView, 0, 0);
|
||||
@@ -67,7 +66,7 @@ public class DeliveryStatusView extends FrameLayout {
|
||||
public void setPending() {
|
||||
this.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.setVisibility(View.VISIBLE);
|
||||
pendingIndicator.startAnimation(ROTATION_ANIMATION);
|
||||
pendingIndicator.startAnimation(rotationAnimation);
|
||||
sentIndicator.setVisibility(View.GONE);
|
||||
deliveredIndicator.setVisibility(View.GONE);
|
||||
readIndicator.setVisibility(View.GONE);
|
||||
|
||||
@@ -5,14 +5,10 @@ import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -20,7 +16,6 @@ import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
@@ -32,9 +27,6 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
|
||||
private static final String TAG = Log.tag(FromTextView.class);
|
||||
|
||||
private final static Typeface BOLD_TYPEFACE = Typeface.create("sans-serif-medium", Typeface.NORMAL);
|
||||
private final static Typeface LIGHT_TYPEFACE = Typeface.create("sans-serif", Typeface.NORMAL);
|
||||
|
||||
public FromTextView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
@@ -52,8 +44,10 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
||||
String fromString = recipient.getDisplayName(getContext());
|
||||
setText(recipient, recipient.getDisplayName(getContext()), read, suffix);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
SpannableString fromSpan = new SpannableString(fromString);
|
||||
fromSpan.setSpan(getFontSpan(!read), 0, fromSpan.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
@@ -10,8 +10,6 @@ import androidx.appcompat.app.AlertDialog;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
||||
@@ -61,7 +61,7 @@ class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : Line
|
||||
}
|
||||
|
||||
val widthDp: Float = ViewUtil.pxToDp(width.toFloat())
|
||||
val minButtonWidthDp = 70
|
||||
val minButtonWidthDp = 80
|
||||
val maxButtons: Int = (widthDp / minButtonWidthDp).toInt()
|
||||
val usableButtonCount = when {
|
||||
items.size <= maxButtons -> items.size
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.recyclerview;
|
||||
|
||||
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class DeleteItemAnimator extends DefaultItemAnimator {
|
||||
|
||||
public DeleteItemAnimator() {
|
||||
setSupportsChangeAnimations(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateAdd(RecyclerView.ViewHolder viewHolder) {
|
||||
dispatchAddFinished(viewHolder);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean animateMove(RecyclerView.ViewHolder viewHolder, int fromX, int fromY, int toX, int toY) {
|
||||
dispatchMoveFinished(viewHolder);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
@@ -31,18 +32,13 @@ public class DozeReminder extends Reminder {
|
||||
context.startActivity(intent);
|
||||
});
|
||||
|
||||
setDismissListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
TextSecurePreferences.setPromptedOptimizeDoze(context, true);
|
||||
}
|
||||
});
|
||||
setDismissListener(v -> TextSecurePreferences.setPromptedOptimizeDoze(context, true));
|
||||
}
|
||||
|
||||
public static boolean isEligible(Context context) {
|
||||
return TextSecurePreferences.isFcmDisabled(context) &&
|
||||
return !SignalStore.account().isFcmEnabled() &&
|
||||
!TextSecurePreferences.hasPromptedOptimizeDoze(context) &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||
Build.VERSION.SDK_INT >= 23 &&
|
||||
!((PowerManager)context.getSystemService(Context.POWER_SERVICE)).isIgnoringBatteryOptimizations(context.getPackageName());
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.reminder;
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
@@ -21,6 +22,6 @@ public class PushRegistrationReminder extends Reminder {
|
||||
}
|
||||
|
||||
public static boolean isEligible(Context context) {
|
||||
return !TextSecurePreferences.isPushRegistered(context);
|
||||
return !SignalStore.account().isRegistered();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.models.AsyncSwitch
|
||||
import org.thoughtcrime.securesms.components.settings.models.Button
|
||||
import org.thoughtcrime.securesms.components.settings.models.Space
|
||||
import org.thoughtcrime.securesms.components.settings.models.Text
|
||||
@@ -37,6 +38,7 @@ class DSLSettingsAdapter : MappingAdapter() {
|
||||
Text.register(this)
|
||||
Space.register(this)
|
||||
Button.register(this)
|
||||
AsyncSwitch.register(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ abstract class DSLSettingsBottomSheetFragment(
|
||||
override val peekHeightPercentage: Float = 1f
|
||||
) : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
protected lateinit var recyclerView: RecyclerView
|
||||
private set
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(layoutId, container, false)
|
||||
@@ -33,6 +34,7 @@ abstract class DSLSettingsBottomSheetFragment(
|
||||
|
||||
recyclerView.layoutManager = layoutManagerProducer(requireContext())
|
||||
recyclerView.adapter = adapter
|
||||
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||
|
||||
bindAdapter(adapter)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ sealed class DSLSettingsText {
|
||||
}
|
||||
|
||||
object Title2BoldModifier : TextAppearanceModifier(R.style.TextAppearance_Signal_Title2_Bold)
|
||||
object Body1Modifier : TextAppearanceModifier(R.style.Signal_Text_Body)
|
||||
object Body1BoldModifier : TextAppearanceModifier(R.style.TextAppearance_Signal_Body1_Bold)
|
||||
|
||||
open class TextAppearanceModifier(@StyleRes private val textAppearance: Int) : Modifier {
|
||||
@@ -70,4 +69,10 @@ sealed class DSLSettingsText {
|
||||
return SpanUtil.textAppearance(context, textAppearance, charSequence)
|
||||
}
|
||||
}
|
||||
|
||||
object BoldModifier : Modifier {
|
||||
override fun modify(context: Context, charSequence: CharSequence): CharSequence {
|
||||
return SpanUtil.bold(charSequence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,49 +3,33 @@ package org.thoughtcrime.securesms.components.settings.app
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.navigation.NavDirections
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import org.thoughtcrime.securesms.MainActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.BoostRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.BoostViewModel
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.subscribe.SubscribeViewModel
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.CachedInflater
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
|
||||
private const val START_LOCATION = "app.settings.start.location"
|
||||
private const val NOTIFICATION_CATEGORY = "android.intent.category.NOTIFICATION_PREFERENCES"
|
||||
private const val STATE_WAS_CONFIGURATION_UPDATED = "app.settings.state.configuration.updated"
|
||||
|
||||
class AppSettingsActivity : DSLSettingsActivity() {
|
||||
class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
|
||||
|
||||
private var wasConfigurationUpdated = false
|
||||
|
||||
private val donationRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) }
|
||||
private val subscribeViewModel: SubscribeViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
SubscribeViewModel.Factory(SubscriptionsRepository(ApplicationDependencies.getDonationsService()), donationRepository, FETCH_SUBSCRIPTION_TOKEN_REQUEST_CODE)
|
||||
}
|
||||
)
|
||||
|
||||
private val boostViewModel: BoostViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
BoostViewModel.Factory(BoostRepository(), donationRepository, FETCH_BOOST_TOKEN_REQUEST_CODE)
|
||||
}
|
||||
)
|
||||
override val donationPaymentRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) }
|
||||
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
warmDonationViewModels()
|
||||
|
||||
if (intent?.hasExtra(ARG_NAV_GRAPH) != true) {
|
||||
intent?.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||
}
|
||||
@@ -64,6 +48,8 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
|
||||
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
|
||||
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToSubscriptions()
|
||||
StartLocation.BOOST -> AppSettingsFragmentDirections.actionAppSettingsFragmentToBoostsFragment()
|
||||
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +76,12 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(STATE_WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated)
|
||||
@@ -105,15 +97,10 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
subscribeViewModel.onActivityResult(requestCode, resultCode, data)
|
||||
boostViewModel.onActivityResult(requestCode, resultCode, data)
|
||||
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val FETCH_SUBSCRIPTION_TOKEN_REQUEST_CODE = 1000
|
||||
private const val FETCH_BOOST_TOKEN_REQUEST_CODE = 2000
|
||||
|
||||
@JvmStatic
|
||||
fun home(context: Context): Intent = getIntentForStartLocation(context, StartLocation.HOME)
|
||||
|
||||
@@ -138,6 +125,12 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
@JvmStatic
|
||||
fun subscriptions(context: Context): Intent = getIntentForStartLocation(context, StartLocation.SUBSCRIPTIONS)
|
||||
|
||||
@JvmStatic
|
||||
fun boost(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BOOST)
|
||||
|
||||
@JvmStatic
|
||||
fun manageSubscriptions(context: Context): Intent = getIntentForStartLocation(context, StartLocation.MANAGE_SUBSCRIPTIONS)
|
||||
|
||||
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
|
||||
return Intent(context, AppSettingsActivity::class.java)
|
||||
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings)
|
||||
@@ -145,13 +138,6 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun warmDonationViewModels() {
|
||||
if (FeatureFlags.donorBadges()) {
|
||||
subscribeViewModel
|
||||
boostViewModel
|
||||
}
|
||||
}
|
||||
|
||||
private enum class StartLocation(val code: Int) {
|
||||
HOME(0),
|
||||
BACKUPS(1),
|
||||
@@ -159,7 +145,9 @@ class AppSettingsActivity : DSLSettingsActivity() {
|
||||
PROXY(3),
|
||||
NOTIFICATIONS(4),
|
||||
CHANGE_NUMBER(5),
|
||||
SUBSCRIPTIONS(6);
|
||||
SUBSCRIPTIONS(6),
|
||||
BOOST(7),
|
||||
MANAGE_SUBSCRIPTIONS(8);
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: Int?): StartLocation {
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.PlayServicesUtil
|
||||
|
||||
class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__menu_settings) {
|
||||
|
||||
@@ -143,27 +144,32 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
}
|
||||
)
|
||||
|
||||
if (FeatureFlags.donorBadges()) {
|
||||
if (FeatureFlags.donorBadges() && PlayServicesUtil.getPlayServicesStatus(requireContext()) == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
|
||||
customPref(
|
||||
SubscriptionPreference(
|
||||
title = DSLSettingsText.from(R.string.preferences__subscription),
|
||||
title = DSLSettingsText.from(
|
||||
if (state.hasActiveSubscription) {
|
||||
R.string.preferences__subscription
|
||||
} else {
|
||||
R.string.preferences__become_a_signal_sustainer
|
||||
}
|
||||
),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),
|
||||
isActive = state.hasActiveSubscription,
|
||||
onClick = { isActive ->
|
||||
findNavController()
|
||||
.navigate(
|
||||
AppSettingsFragmentDirections.actionAppSettingsFragmentToSubscriptions()
|
||||
.setSkipToSubscribe(!isActive)
|
||||
)
|
||||
if (isActive) {
|
||||
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToManageDonationsFragment())
|
||||
} else {
|
||||
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToSubscribeFragment())
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
// TODO [alex] -- clap
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__signal_boost),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_boost_24),
|
||||
onClick = {
|
||||
findNavController().navigate(R.id.action_appSettingsFragment_to_boostsFragment)
|
||||
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToBoostsFragment())
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@@ -198,6 +204,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
|
||||
override fun areItemsTheSame(newItem: SubscriptionPreference): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: SubscriptionPreference): Boolean {
|
||||
return super.areContentsTheSame(newItem) && isActive == newItem.isActive
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRepository) : ViewModel() {
|
||||
@@ -36,7 +39,14 @@ class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRep
|
||||
}
|
||||
|
||||
subscriptionsRepository.getActiveSubscription().subscribeBy(
|
||||
onSuccess = { subscription -> store.update { it.copy(hasActiveSubscription = subscription.isActive) } },
|
||||
onSuccess = { subscription -> store.update { it.copy(hasActiveSubscription = subscription.activeSubscription != null) } },
|
||||
onError = { throwable ->
|
||||
if (throwable.isNotFoundException()) {
|
||||
Log.w(TAG, "Could not load active subscription due to unset SubscriberId (404).")
|
||||
}
|
||||
|
||||
Log.w(TAG, "Could not load active subscription", throwable)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,4 +55,12 @@ class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRep
|
||||
return modelClass.cast(AppSettingsViewModel(subscriptionsRepository)) as T
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(AppSettingsViewModel::class.java)
|
||||
}
|
||||
|
||||
private fun Throwable.isNotFoundException(): Boolean {
|
||||
return this is PushNetworkException && this.cause is NotFoundException || this is NotFoundException
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.Objects
|
||||
|
||||
private val TAG: String = Log.tag(ChangeNumberLockActivity::class.java)
|
||||
@@ -53,11 +52,11 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
disposables.add(
|
||||
changeNumberRepository.whoAmI()
|
||||
.flatMap { whoAmI ->
|
||||
if (Objects.equals(whoAmI.number, TextSecurePreferences.getLocalNumber(this))) {
|
||||
if (Objects.equals(whoAmI.number, SignalStore.account().e164)) {
|
||||
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
|
||||
Single.just(false)
|
||||
} else {
|
||||
Log.i(TAG, "Local (${TextSecurePreferences.getLocalNumber(this)}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
|
||||
changeNumberRepository.changeLocalNumber(whoAmI.number)
|
||||
.map { true }
|
||||
}
|
||||
@@ -72,7 +71,7 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed)
|
||||
.setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(TextSecurePreferences.getLocalNumber(this))))
|
||||
.setMessage(getString(R.string.ChangeNumberLockActivity__your_number_has_been_confirmed_as_s, PhoneNumberFormatter.prettyPrint(SignalStore.account().e164!!)))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
startActivity(MainActivity.clearTop(this))
|
||||
finish()
|
||||
|
||||
@@ -5,7 +5,7 @@ import androidx.annotation.WorkerThread
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
@@ -60,9 +59,9 @@ class ChangeNumberRepository(private val context: Context) {
|
||||
|
||||
@WorkerThread
|
||||
fun changeLocalNumber(e164: String): Single<Unit> {
|
||||
DatabaseFactory.getRecipientDatabase(context).updateSelfPhone(e164)
|
||||
SignalDatabase.recipients.updateSelfPhone(e164)
|
||||
|
||||
TextSecurePreferences.setLocalNumber(context, e164)
|
||||
SignalStore.account().setE164(e164)
|
||||
|
||||
ApplicationDependencies.closeConnections()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.registration.VerifyProcessor
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.BaseRegistrationViewModel
|
||||
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.util.Objects
|
||||
@@ -167,8 +166,8 @@ class ChangeNumberViewModel(
|
||||
|
||||
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
|
||||
val context: Application = ApplicationDependencies.getApplication()
|
||||
val localNumber: String = TextSecurePreferences.getLocalNumber(context)
|
||||
val password: String = TextSecurePreferences.getPushServerPassword(context)
|
||||
val localNumber: String = SignalStore.account().e164!!
|
||||
val password: String = SignalStore.account().servicePassword!!
|
||||
|
||||
val viewModel = ChangeNumberViewModel(
|
||||
localNumber = localNumber,
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.chats
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -19,7 +19,7 @@ class ChatsSettingsRepository {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val isLinkPreviewsEnabled = SignalStore.settings().isLinkPreviewsEnabled
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
ApplicationDependencies.getJobManager().add(
|
||||
MultiDeviceConfigurationUpdateJob(
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.data
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
|
||||
class DataAndStorageSettingsRepository {
|
||||
@@ -11,7 +11,7 @@ class DataAndStorageSettingsRepository {
|
||||
|
||||
fun getTotalStorageUse(consumer: (Long) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val breakdown = DatabaseFactory.getMediaDatabase(context).storageBreakdown
|
||||
val breakdown = SignalDatabase.media.storageBreakdown
|
||||
|
||||
consumer(listOf(breakdown.audioSize, breakdown.documentSize, breakdown.photoSize, breakdown.videoSize).sum())
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
@@ -24,8 +24,11 @@ import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.RemoteConfigRefreshJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob
|
||||
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.payments.DataExportUtil
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask
|
||||
|
||||
class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__internal_preferences) {
|
||||
@@ -317,6 +320,17 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (FeatureFlags.donorBadges() && SignalStore.donationsValues().getSubscriber() != null) {
|
||||
sectionHeaderPref(R.string.preferences__internal_badges)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__internal_badges_enqueue_redemption),
|
||||
onClick = {
|
||||
enqueueSubscriptionRedemption()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,13 +399,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
}
|
||||
|
||||
private fun clearAllSenderKeyState() {
|
||||
DatabaseFactory.getSenderKeyDatabase(requireContext()).deleteAll()
|
||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
||||
SignalDatabase.senderKeys.deleteAll()
|
||||
SignalDatabase.senderKeyShared.deleteAll()
|
||||
Toast.makeText(context, "Deleted all sender key state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun clearAllSenderKeySharedState() {
|
||||
DatabaseFactory.getSenderKeySharedDatabase(requireContext()).deleteAll()
|
||||
SignalDatabase.senderKeyShared.deleteAll()
|
||||
Toast.makeText(context, "Deleted all sender key shared state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -399,4 +413,8 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
|
||||
LocalMetricsDatabase.getInstance(ApplicationDependencies.getApplication()).clear()
|
||||
Toast.makeText(context, "Cleared all local metrics state.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun enqueueSubscriptionRedemption() {
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.privacy
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -16,7 +16,7 @@ class PrivacySettingsRepository {
|
||||
|
||||
fun getBlockedCount(consumer: (Int) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
val recipientDatabase = SignalDatabase.recipients
|
||||
|
||||
consumer(recipientDatabase.blocked.count)
|
||||
}
|
||||
@@ -24,7 +24,7 @@ class PrivacySettingsRepository {
|
||||
|
||||
fun syncReadReceiptState() {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
ApplicationDependencies.getJobManager().add(
|
||||
MultiDeviceConfigurationUpdateJob(
|
||||
@@ -40,7 +40,7 @@ class PrivacySettingsRepository {
|
||||
fun syncTypingIndicatorsState() {
|
||||
val enabled = TextSecurePreferences.isTypingIndicatorsEnabled(context)
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
ApplicationDependencies.getJobManager().add(
|
||||
MultiDeviceConfigurationUpdateJob(
|
||||
|
||||
@@ -18,11 +18,11 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__advanced) {
|
||||
@@ -168,7 +168,7 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences
|
||||
|
||||
private fun getPushToggleSummary(isPushEnabled: Boolean): String {
|
||||
return if (isPushEnabled) {
|
||||
PhoneNumberFormatter.prettyPrint(TextSecurePreferences.getLocalNumber(requireContext()))
|
||||
PhoneNumberFormatter.prettyPrint(SignalStore.account().e164!!)
|
||||
} else {
|
||||
getString(R.string.preferences__free_private_messages_and_calls)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.google.android.gms.tasks.Tasks
|
||||
import com.google.firebase.installations.FirebaseInstallations
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -30,7 +30,7 @@ class AdvancedPrivacySettingsRepository(private val context: Context) {
|
||||
} catch (e: AuthorizationFailedException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
if (!TextSecurePreferences.isFcmDisabled(context)) {
|
||||
if (SignalStore.account().fcmEnabled) {
|
||||
Tasks.await(FirebaseInstallations.getInstance().delete())
|
||||
}
|
||||
DisablePushMessagesResult.SUCCESS
|
||||
@@ -51,7 +51,7 @@ class AdvancedPrivacySettingsRepository(private val context: Context) {
|
||||
|
||||
fun syncShowSealedSenderIconState() {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().id)
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
ApplicationDependencies.getJobManager().add(
|
||||
MultiDeviceConfigurationUpdateJob(
|
||||
|
||||
@@ -28,7 +28,7 @@ class AdvancedPrivacySettingsViewModel(
|
||||
repository.disablePushMessages {
|
||||
when (it) {
|
||||
AdvancedPrivacySettingsRepository.DisablePushMessagesResult.SUCCESS -> {
|
||||
TextSecurePreferences.setPushRegistered(ApplicationDependencies.getApplication(), false)
|
||||
SignalStore.account().setRegistered(false)
|
||||
SignalStore.registrationValues().clearRegistrationComplete()
|
||||
SignalStore.registrationValues().clearHasUploadedProfile()
|
||||
}
|
||||
@@ -63,7 +63,7 @@ class AdvancedPrivacySettingsViewModel(
|
||||
}
|
||||
|
||||
private fun getState() = AdvancedPrivacySettingsState(
|
||||
isPushEnabled = TextSecurePreferences.isPushRegistered(ApplicationDependencies.getApplication()),
|
||||
isPushEnabled = SignalStore.account().isRegistered,
|
||||
alwaysRelayCalls = TextSecurePreferences.isTurnOnly(ApplicationDependencies.getApplication()),
|
||||
showSealedSenderStatusIcon = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(
|
||||
ApplicationDependencies.getApplication()
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.content.Context
|
||||
import androidx.annotation.WorkerThread
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException
|
||||
import org.thoughtcrime.securesms.groups.GroupManager
|
||||
@@ -36,7 +36,7 @@ class ExpireTimerSettingsRepository(val context: Context) {
|
||||
consumer.invoke(Result.failure(e))
|
||||
}
|
||||
} else {
|
||||
DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipientId, newExpirationTime)
|
||||
SignalDatabase.recipients.setExpireMessages(recipientId, newExpirationTime)
|
||||
val outgoingMessage = OutgoingExpirationUpdateMessage(Recipient.resolved(recipientId), System.currentTimeMillis(), newExpirationTime * 1000L)
|
||||
MessageSender.send(context, outgoingMessage, getThreadId(recipientId), false, null, null)
|
||||
consumer.invoke(Result.success(newExpirationTime))
|
||||
@@ -46,7 +46,7 @@ class ExpireTimerSettingsRepository(val context: Context) {
|
||||
|
||||
@WorkerThread
|
||||
private fun getThreadId(recipientId: RecipientId): Long {
|
||||
val threadDatabase: ThreadDatabase = DatabaseFactory.getThreadDatabase(context)
|
||||
val threadDatabase: ThreadDatabase = SignalDatabase.threads
|
||||
val recipient: Recipient = Recipient.resolved(recipientId)
|
||||
return threadDatabase.getOrCreateThreadIdFor(recipient)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.badges.models.Badge
|
||||
sealed class DonationEvent {
|
||||
class GooglePayUnavailableError(val throwable: Throwable) : DonationEvent()
|
||||
object RequestTokenSuccess : DonationEvent()
|
||||
object RequestTokenError : DonationEvent()
|
||||
class RequestTokenError(val throwable: Throwable) : DonationEvent()
|
||||
class PaymentConfirmationError(val throwable: Throwable) : DonationEvent()
|
||||
class PaymentConfirmationSuccess(val badge: Badge) : DonationEvent()
|
||||
class SubscriptionCancellationFailed(val throwable: Throwable) : DonationEvent()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
class DonationExceptions {
|
||||
class SetupFailed(reason: Throwable) : Exception(reason)
|
||||
object TimedOutWaitingForTokenRedemption : Exception()
|
||||
object RedemptionFailed : Exception()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription
|
||||
|
||||
import android.content.Intent
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
|
||||
interface DonationPaymentComponent {
|
||||
val donationPaymentRepository: DonationPaymentRepository
|
||||
val googlePayResultPublisher: Subject<GooglePayResult>
|
||||
|
||||
class GooglePayResult(val requestCode: Int, val resultCode: Int, val data: Intent?)
|
||||
}
|
||||
@@ -6,19 +6,30 @@ import com.google.android.gms.wallet.PaymentData
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.signal.donations.GooglePayPaymentSource
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriptionClientSecret
|
||||
import org.whispersystems.signalservice.internal.EmptyResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -41,12 +52,25 @@ import java.util.concurrent.TimeUnit
|
||||
*/
|
||||
class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFetcher, StripeApi.SetupIntentHelper {
|
||||
|
||||
private val application = activity.application
|
||||
private val googlePayApi = GooglePayApi(activity, StripeApi.Gateway(Environment.Donations.STRIPE_CONFIGURATION), Environment.Donations.GOOGLE_PAY_CONFIGURATION)
|
||||
private val stripeApi = StripeApi(Environment.Donations.STRIPE_CONFIGURATION, this, this, ApplicationDependencies.getOkHttpClient())
|
||||
|
||||
fun isGooglePayAvailable(): Completable = googlePayApi.queryIsReadyToPay()
|
||||
|
||||
fun scheduleSyncForAccountRecordChange() {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
scheduleSyncForAccountRecordChangeSync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleSyncForAccountRecordChangeSync() {
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
|
||||
fun requestTokenFromGooglePay(price: FiatMoney, label: String, requestCode: Int) {
|
||||
Log.d(TAG, "Requesting a token from google pay...")
|
||||
googlePayApi.requestPayment(price, label, requestCode)
|
||||
}
|
||||
|
||||
@@ -57,168 +81,237 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
|
||||
expectedRequestCode: Int,
|
||||
paymentsRequestCallback: GooglePayApi.PaymentRequestCallback
|
||||
) {
|
||||
Log.d(TAG, "Processing possible google pay result...")
|
||||
googlePayApi.onActivityResult(requestCode, resultCode, data, expectedRequestCode, paymentsRequestCallback)
|
||||
}
|
||||
|
||||
fun continuePayment(price: FiatMoney, paymentData: PaymentData): Completable {
|
||||
return stripeApi.createPaymentIntent(price)
|
||||
Log.d(TAG, "Creating payment intent...", true)
|
||||
return stripeApi.createPaymentIntent(price, application.getString(R.string.Boost__thank_you_for_your_donation))
|
||||
.onErrorResumeNext { Single.error(DonationExceptions.SetupFailed(it)) }
|
||||
.flatMapCompletable { result ->
|
||||
Log.d(TAG, "Created payment intent.", true)
|
||||
when (result) {
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Completable.error(Exception("Amount is too small"))
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Completable.error(Exception("Amount is too large"))
|
||||
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Completable.error(Exception("Currency is not supported"))
|
||||
is StripeApi.CreatePaymentIntentResult.Success -> stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), result.paymentIntent)
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooSmall -> Completable.error(DonationExceptions.SetupFailed(Exception("Boost amount is too small")))
|
||||
is StripeApi.CreatePaymentIntentResult.AmountIsTooLarge -> Completable.error(DonationExceptions.SetupFailed(Exception("Boost amount is too large")))
|
||||
is StripeApi.CreatePaymentIntentResult.CurrencyIsNotSupported -> Completable.error(DonationExceptions.SetupFailed(Exception("Boost currency is not supported")))
|
||||
is StripeApi.CreatePaymentIntentResult.Success -> confirmPayment(paymentData, result.paymentIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun continueSubscriptionSetup(paymentData: PaymentData): Completable {
|
||||
Log.d(TAG, "Continuing subscription setup...", true)
|
||||
return stripeApi.createSetupIntent()
|
||||
.flatMapCompletable { result ->
|
||||
stripeApi.confirmSetupIntent(GooglePayPaymentSource(paymentData), result.setupIntent)
|
||||
Log.d(TAG, "Retrieved SetupIntent, confirming...", true)
|
||||
stripeApi.confirmSetupIntent(GooglePayPaymentSource(paymentData), result.setupIntent).doOnComplete {
|
||||
Log.d(TAG, "Confirmed SetupIntent...", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelActiveSubscription(): Completable {
|
||||
Log.d(TAG, "Canceling active subscription...", true)
|
||||
val localSubscriber = SignalStore.donationsValues().requireSubscriber()
|
||||
return ApplicationDependencies.getDonationsService().cancelSubscription(localSubscriber.subscriberId).flatMapCompletable {
|
||||
when {
|
||||
it.status == 200 -> Completable.complete()
|
||||
it.applicationError.isPresent -> Completable.error(it.applicationError.get())
|
||||
it.executionError.isPresent -> Completable.error(it.executionError.get())
|
||||
else -> Completable.error(AssertionError("Something bad happened"))
|
||||
}
|
||||
}
|
||||
return ApplicationDependencies.getDonationsService()
|
||||
.cancelSubscription(localSubscriber.subscriberId)
|
||||
.flatMap(ServiceResponse<EmptyResponse>::flattenResult)
|
||||
.ignoreElement()
|
||||
.doOnComplete { Log.d(TAG, "Cancelled active subscription.", true) }
|
||||
}
|
||||
|
||||
fun ensureSubscriberId(): Completable {
|
||||
Log.d(TAG, "Ensuring SubscriberId exists on Signal service...", true)
|
||||
val subscriberId = SignalStore.donationsValues().getSubscriber()?.subscriberId ?: SubscriberId.generate()
|
||||
return ApplicationDependencies
|
||||
.getDonationsService()
|
||||
.putSubscription(subscriberId)
|
||||
.flatMapCompletable {
|
||||
when {
|
||||
it.status == 200 -> Completable.complete()
|
||||
it.applicationError.isPresent -> Completable.error(it.applicationError.get())
|
||||
it.executionError.isPresent -> Completable.error(it.executionError.get())
|
||||
else -> Completable.error(AssertionError("Something bad happened"))
|
||||
}
|
||||
}
|
||||
.flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement()
|
||||
.doOnComplete {
|
||||
Log.d(TAG, "Successfully set SubscriberId exists on Signal service.", true)
|
||||
|
||||
SignalStore
|
||||
.donationsValues()
|
||||
.setSubscriber(Subscriber(subscriberId, SignalStore.donationsValues().getSubscriptionCurrency().currencyCode))
|
||||
|
||||
scheduleSyncForAccountRecordChangeSync()
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmPayment(paymentData: PaymentData, paymentIntent: StripeApi.PaymentIntent): Completable {
|
||||
Log.d(TAG, "Confirming payment intent...", true)
|
||||
val confirmPayment = stripeApi.confirmPaymentIntent(GooglePayPaymentSource(paymentData), paymentIntent)
|
||||
val waitOnRedemption = Completable.create {
|
||||
Log.d(TAG, "Confirmed payment intent.", true)
|
||||
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
|
||||
BoostReceiptRequestResponseJob.createJobChain(paymentIntent).enqueue { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
finalJobState = jobState
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
when (finalJobState) {
|
||||
JobTracker.JobState.SUCCESS -> {
|
||||
Log.d(TAG, "Boost request response job chain succeeded.", true)
|
||||
it.onComplete()
|
||||
}
|
||||
JobTracker.JobState.FAILURE -> {
|
||||
Log.d(TAG, "Boost request response job chain failed permanently.", true)
|
||||
it.onError(DonationExceptions.RedemptionFailed)
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Boost request response job chain ignored due to in-progress jobs.", true)
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Boost redemption timed out waiting for job completion.", true)
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "Boost redemption job interrupted", e, true)
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
}
|
||||
}
|
||||
|
||||
return confirmPayment.andThen(waitOnRedemption)
|
||||
}
|
||||
|
||||
fun setSubscriptionLevel(subscriptionLevel: String): Completable {
|
||||
return getOrCreateLevelUpdateOperation(subscriptionLevel)
|
||||
.flatMapCompletable { levelUpdateOperation ->
|
||||
val subscriber = SignalStore.donationsValues().requireSubscriber()
|
||||
|
||||
Log.d(TAG, "Attempting to set user subscription level to $subscriptionLevel", true)
|
||||
ApplicationDependencies.getDonationsService().updateSubscriptionLevel(
|
||||
subscriber.subscriberId,
|
||||
subscriptionLevel,
|
||||
subscriber.currencyCode,
|
||||
levelUpdateOperation.idempotencyKey.serialize()
|
||||
).flatMapCompletable { response ->
|
||||
when {
|
||||
response.status == 200 -> Completable.complete()
|
||||
response.applicationError.isPresent -> Completable.error(response.applicationError.get())
|
||||
response.executionError.isPresent -> Completable.error(response.executionError.get())
|
||||
else -> Completable.error(AssertionError("should never happen"))
|
||||
levelUpdateOperation.idempotencyKey.serialize(),
|
||||
SubscriptionReceiptRequestResponseJob.MUTEX
|
||||
).flatMapCompletable {
|
||||
if (it.status == 200 || it.status == 204) {
|
||||
Log.d(TAG, "Successfully set user subscription to level $subscriptionLevel with response code ${it.status}", true)
|
||||
SignalStore.donationsValues().clearUserManuallyCancelled()
|
||||
SignalStore.donationsValues().clearLevelOperations()
|
||||
LevelUpdate.updateProcessingState(false)
|
||||
Completable.complete()
|
||||
} else {
|
||||
if (it.applicationError.isPresent) {
|
||||
Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel with response code ${it.status}", it.applicationError.get(), true)
|
||||
SignalStore.donationsValues().clearLevelOperations()
|
||||
} else {
|
||||
Log.w(TAG, "Failed to set user subscription to level $subscriptionLevel", it.executionError.orNull(), true)
|
||||
}
|
||||
|
||||
LevelUpdate.updateProcessingState(false)
|
||||
it.flattenResult().ignoreElement()
|
||||
}
|
||||
}.andThen {
|
||||
SignalStore.donationsValues().clearLevelOperation(levelUpdateOperation)
|
||||
it.onComplete()
|
||||
}.andThen {
|
||||
val jobIds = SubscriptionReceiptRequestResponseJob.enqueueSubscriptionContinuation()
|
||||
val countDownLatch = CountDownLatch(2)
|
||||
Log.d(TAG, "Enqueuing request response job chain.", true)
|
||||
val countDownLatch = CountDownLatch(1)
|
||||
var finalJobState: JobTracker.JobState? = null
|
||||
|
||||
val firstJobListener = JobTracker.JobListener { _, jobState ->
|
||||
SubscriptionReceiptRequestResponseJob.createSubscriptionContinuationJobChain().enqueue { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
finalJobState = jobState
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
val secondJobListener = JobTracker.JobListener { _, jobState ->
|
||||
if (jobState.isComplete) {
|
||||
countDownLatch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager().addListener(jobIds.first(), firstJobListener)
|
||||
ApplicationDependencies.getJobManager().addListener(jobIds.second(), secondJobListener)
|
||||
|
||||
try {
|
||||
if (!countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
|
||||
when (finalJobState) {
|
||||
JobTracker.JobState.SUCCESS -> {
|
||||
Log.d(TAG, "Subscription request response job chain succeeded.", true)
|
||||
it.onComplete()
|
||||
}
|
||||
JobTracker.JobState.FAILURE -> {
|
||||
Log.d(TAG, "Subscription request response job chain failed permanently.", true)
|
||||
it.onError(DonationExceptions.RedemptionFailed)
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Subscription request response job chain ignored due to in-progress jobs.", true)
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
it.onComplete()
|
||||
Log.d(TAG, "Subscription request response job timed out.", true)
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.w(TAG, "Subscription request response interrupted.", e, true)
|
||||
it.onError(DonationExceptions.TimedOutWaitingForTokenRedemption)
|
||||
}
|
||||
}
|
||||
}.doOnError {
|
||||
LevelUpdate.updateProcessingState(false)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun getOrCreateLevelUpdateOperation(subscriptionLevel: String): Single<LevelUpdateOperation> = Single.fromCallable {
|
||||
val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation()
|
||||
if (levelUpdateOperation == null || subscriptionLevel != levelUpdateOperation.level) {
|
||||
Log.d(TAG, "Retrieving level update operation for $subscriptionLevel")
|
||||
val levelUpdateOperation = SignalStore.donationsValues().getLevelOperation(subscriptionLevel)
|
||||
if (levelUpdateOperation == null) {
|
||||
val newOperation = LevelUpdateOperation(
|
||||
idempotencyKey = IdempotencyKey.generate(),
|
||||
level = subscriptionLevel
|
||||
)
|
||||
|
||||
SignalStore.donationsValues().setLevelOperation(newOperation)
|
||||
LevelUpdate.updateProcessingState(true)
|
||||
Log.d(TAG, "Created a new operation for $subscriptionLevel")
|
||||
newOperation
|
||||
} else {
|
||||
LevelUpdate.updateProcessingState(true)
|
||||
Log.d(TAG, "Reusing operation for $subscriptionLevel")
|
||||
levelUpdateOperation
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchPaymentIntent(price: FiatMoney, description: String?): Single<StripeApi.PaymentIntent> {
|
||||
Log.d(TAG, "Fetching payment intent from Signal service...")
|
||||
return ApplicationDependencies
|
||||
.getDonationsService()
|
||||
.createDonationIntentWithAmount(price.minimumUnitPrecisionString, price.currency.currencyCode)
|
||||
.flatMap { response ->
|
||||
when {
|
||||
response.status == 200 -> Single.just(StripeApi.PaymentIntent(response.result.get().id, response.result.get().clientSecret))
|
||||
response.executionError.isPresent -> Single.error(response.executionError.get())
|
||||
response.applicationError.isPresent -> Single.error(response.applicationError.get())
|
||||
else -> Single.error(AssertionError("should never get here"))
|
||||
}
|
||||
.createDonationIntentWithAmount(price.minimumUnitPrecisionString, price.currency.currencyCode, description)
|
||||
.flatMap(ServiceResponse<SubscriptionClientSecret>::flattenResult)
|
||||
.map {
|
||||
StripeApi.PaymentIntent(it.id, it.clientSecret)
|
||||
}.doOnSuccess {
|
||||
Log.d(TAG, "Got payment intent from Signal service!")
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchSetupIntent(): Single<StripeApi.SetupIntent> {
|
||||
return Single.fromCallable {
|
||||
SignalStore.donationsValues().requireSubscriber()
|
||||
}.flatMap {
|
||||
ApplicationDependencies.getDonationsService().createSubscriptionPaymentMethod(it.subscriberId)
|
||||
}.flatMap { response ->
|
||||
when {
|
||||
response.status == 200 -> Single.just(StripeApi.SetupIntent(response.result.get().id, response.result.get().clientSecret))
|
||||
response.executionError.isPresent -> Single.error(response.executionError.get())
|
||||
response.applicationError.isPresent -> Single.error(response.applicationError.get())
|
||||
else -> Single.error(AssertionError("should never get here"))
|
||||
Log.d(TAG, "Fetching setup intent from Signal service...")
|
||||
return Single.fromCallable { SignalStore.donationsValues().requireSubscriber() }
|
||||
.flatMap { ApplicationDependencies.getDonationsService().createSubscriptionPaymentMethod(it.subscriberId) }
|
||||
.flatMap(ServiceResponse<SubscriptionClientSecret>::flattenResult)
|
||||
.map { StripeApi.SetupIntent(it.id, it.clientSecret) }
|
||||
.doOnSuccess {
|
||||
Log.d(TAG, "Got setup intent from Signal service!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDefaultPaymentMethod(paymentMethodId: String): Completable {
|
||||
Log.d(TAG, "Setting default payment method via Signal service...")
|
||||
return Single.fromCallable {
|
||||
SignalStore.donationsValues().requireSubscriber()
|
||||
}.flatMap {
|
||||
ApplicationDependencies.getDonationsService().setDefaultPaymentMethodId(it.subscriberId, paymentMethodId)
|
||||
}.flatMapCompletable { response ->
|
||||
when {
|
||||
response.status == 200 -> Completable.complete()
|
||||
response.executionError.isPresent -> Completable.error(response.executionError.get())
|
||||
response.applicationError.isPresent -> Completable.error(response.applicationError.get())
|
||||
else -> Completable.error(AssertionError("Should never get here"))
|
||||
}
|
||||
}.flatMap(ServiceResponse<EmptyResponse>::flattenResult).ignoreElement().doOnComplete {
|
||||
Log.d(TAG, "Set default payment method via Signal service!")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(DonationPaymentRepository::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
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.SubscriptionLevels
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Repository which can query for the user's active subscription as well as a list of available subscriptions,
|
||||
@@ -18,32 +22,32 @@ class SubscriptionsRepository(private val donationsService: DonationsService) {
|
||||
fun getActiveSubscription(): Single<ActiveSubscription> {
|
||||
val localSubscription = SignalStore.donationsValues().getSubscriber()
|
||||
return if (localSubscription != null) {
|
||||
donationsService.getSubscription(localSubscription.subscriberId).flatMap {
|
||||
when {
|
||||
it.status == 200 -> Single.just(it.result.get())
|
||||
it.applicationError.isPresent -> Single.error(it.applicationError.get())
|
||||
it.executionError.isPresent -> Single.error(it.executionError.get())
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
donationsService.getSubscription(localSubscription.subscriberId)
|
||||
.flatMap(ServiceResponse<ActiveSubscription>::flattenResult)
|
||||
} else {
|
||||
Single.just(ActiveSubscription(null))
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubscriptions(currency: Currency): Single<List<Subscription>> = donationsService.subscriptionLevels.map { response ->
|
||||
response.result.transform { subscriptionLevels ->
|
||||
fun getSubscriptions(): Single<List<Subscription>> = donationsService.getSubscriptionLevels(Locale.getDefault())
|
||||
.flatMap(ServiceResponse<SubscriptionLevels>::flattenResult)
|
||||
.map { subscriptionLevels ->
|
||||
subscriptionLevels.levels.map { (code, level) ->
|
||||
Subscription(
|
||||
id = code,
|
||||
title = level.badge.name,
|
||||
name = level.name,
|
||||
badge = Badges.fromServiceBadge(level.badge),
|
||||
price = FiatMoney(level.currencies[currency.currencyCode]!!, currency),
|
||||
prices = level.currencies.filter {
|
||||
PlatformCurrencyUtil
|
||||
.getAvailableCurrencyCodes()
|
||||
.contains(it.key)
|
||||
}.map { (currencyCode, price) ->
|
||||
FiatMoney(price, Currency.getInstance(currencyCode))
|
||||
}.toSet(),
|
||||
level = code.toInt()
|
||||
)
|
||||
}.sortedBy {
|
||||
it.level
|
||||
}
|
||||
}.or(emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.text.Editable
|
||||
import android.text.Spanned
|
||||
import android.text.TextWatcher
|
||||
@@ -7,7 +10,8 @@ import android.text.method.DigitsKeyListener
|
||||
import android.view.View
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.text.isDigitsOnly
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -19,6 +23,8 @@ import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import java.lang.Integer.min
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import java.util.regex.Pattern
|
||||
@@ -28,7 +34,6 @@ import java.util.regex.Pattern
|
||||
* can unlock a corresponding badge for a time determined by the server.
|
||||
*/
|
||||
data class Boost(
|
||||
val badge: Badge,
|
||||
val price: FiatMoney
|
||||
) {
|
||||
|
||||
@@ -45,6 +50,45 @@ data class Boost(
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingModel : PreferenceModel<LoadingModel>() {
|
||||
override fun areItemsTheSame(newItem: LoadingModel): Boolean = true
|
||||
}
|
||||
|
||||
class LoadingViewHolder(itemView: View) : MappingViewHolder<LoadingModel>(itemView) {
|
||||
|
||||
private val animator: Animator = AnimatorSet().apply {
|
||||
val fadeTo25Animator = ObjectAnimator.ofFloat(itemView, "alpha", 0.8f, 0.25f).apply {
|
||||
duration = 1000L
|
||||
}
|
||||
|
||||
val fadeTo80Animator = ObjectAnimator.ofFloat(itemView, "alpha", 0.25f, 0.8f).apply {
|
||||
duration = 300L
|
||||
}
|
||||
|
||||
playSequentially(fadeTo25Animator, fadeTo80Animator)
|
||||
doOnEnd {
|
||||
if (itemView.isAttachedToWindow) {
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(model: LoadingModel) {
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
if (animator.isStarted) {
|
||||
animator.resume()
|
||||
} else {
|
||||
animator.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
animator.pause()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A widget that allows a user to select from six different amounts, or enter a custom amount.
|
||||
*/
|
||||
@@ -53,7 +97,7 @@ data class Boost(
|
||||
val selectedBoost: Boost?,
|
||||
val currency: Currency,
|
||||
override val isEnabled: Boolean,
|
||||
val onBoostClick: (Boost) -> Unit,
|
||||
val onBoostClick: (View, Boost) -> Unit,
|
||||
val isCustomAmountFocused: Boolean,
|
||||
val onCustomAmountChanged: (String) -> Unit,
|
||||
val onCustomAmountFocusChanged: (Boolean) -> Unit,
|
||||
@@ -79,6 +123,15 @@ data class Boost(
|
||||
private val boost6: MaterialButton = itemView.findViewById(R.id.boost_6)
|
||||
private val custom: AppCompatEditText = itemView.findViewById(R.id.boost_custom)
|
||||
|
||||
private val boostButtons: List<MaterialButton>
|
||||
get() {
|
||||
return if (ViewUtil.isLtr(context)) {
|
||||
listOf(boost1, boost2, boost3, boost4, boost5, boost6)
|
||||
} else {
|
||||
listOf(boost3, boost2, boost1, boost6, boost5, boost4)
|
||||
}
|
||||
}
|
||||
|
||||
private var filter: MoneyFilter? = null
|
||||
|
||||
init {
|
||||
@@ -88,15 +141,17 @@ data class Boost(
|
||||
override fun bind(model: SelectionModel) {
|
||||
itemView.isEnabled = model.isEnabled
|
||||
|
||||
model.boosts.zip(listOf(boost1, boost2, boost3, boost4, boost5, boost6)).forEach { (boost, button) ->
|
||||
model.boosts.zip(boostButtons).forEach { (boost, button) ->
|
||||
button.isSelected = boost == model.selectedBoost && !model.isCustomAmountFocused
|
||||
button.text = FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
boost.price,
|
||||
FiatMoneyUtil.formatOptions()
|
||||
FiatMoneyUtil
|
||||
.formatOptions()
|
||||
.trimZerosAfterDecimal()
|
||||
)
|
||||
button.setOnClickListener {
|
||||
model.onBoostClick(boost)
|
||||
model.onBoostClick(it, boost)
|
||||
custom.clearFocus()
|
||||
}
|
||||
}
|
||||
@@ -104,7 +159,7 @@ data class Boost(
|
||||
if (filter == null || filter?.currency != model.currency) {
|
||||
custom.removeTextChangedListener(filter)
|
||||
|
||||
filter = MoneyFilter(model.currency) {
|
||||
filter = MoneyFilter(model.currency, custom) {
|
||||
model.onCustomAmountChanged(it)
|
||||
}
|
||||
|
||||
@@ -120,6 +175,9 @@ data class Boost(
|
||||
|
||||
if (model.isCustomAmountFocused && !custom.hasFocus()) {
|
||||
ViewUtil.focusAndShowKeyboard(custom)
|
||||
} else if (!model.isCustomAmountFocused && custom.hasFocus()) {
|
||||
ViewUtil.hideKeyboard(context, custom)
|
||||
custom.clearFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,11 +192,14 @@ data class Boost(
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
class MoneyFilter(val currency: Currency, private val onCustomAmountChanged: (String) -> Unit = {}) : DigitsKeyListener(), TextWatcher {
|
||||
class MoneyFilter(val currency: Currency, private val text: AppCompatEditText? = null, private val onCustomAmountChanged: (String) -> Unit = {}) : DigitsKeyListener(false, true), TextWatcher {
|
||||
|
||||
val separator = DecimalFormatSymbols.getInstance().decimalSeparator
|
||||
val separatorCount = min(1, currency.defaultFractionDigits)
|
||||
val prefix: String = "${currency.getSymbol(Locale.getDefault())} "
|
||||
val pattern: Pattern = "[0-9]*([.,]){0,$separatorCount}[0-9]{0,${currency.defaultFractionDigits}}".toPattern()
|
||||
val symbol: String = currency.getSymbol(Locale.getDefault())
|
||||
val pattern: Pattern = "[0-9]*([$separator]){0,$separatorCount}[0-9]{0,${currency.defaultFractionDigits}}".toPattern()
|
||||
val symbolPattern: Regex = """\s*${Regex.escape(symbol)}\s*""".toRegex()
|
||||
val leadingZeroesPattern: Regex = """^0*""".toRegex()
|
||||
|
||||
override fun filter(
|
||||
source: CharSequence,
|
||||
@@ -150,7 +211,12 @@ data class Boost(
|
||||
): CharSequence? {
|
||||
|
||||
val result = dest.subSequence(0, dstart).toString() + source.toString() + dest.subSequence(dend, dest.length)
|
||||
val resultWithoutCurrencyPrefix = result.removePrefix(prefix)
|
||||
val resultWithoutCurrencyPrefix = result.removePrefix(symbol).removeSuffix(symbol).trim()
|
||||
|
||||
if (resultWithoutCurrencyPrefix.length == 1 && !resultWithoutCurrencyPrefix.isDigitsOnly() && resultWithoutCurrencyPrefix != separator.toString()) {
|
||||
return dest.subSequence(dstart, dend)
|
||||
}
|
||||
|
||||
val matcher = pattern.matcher(resultWithoutCurrencyPrefix)
|
||||
|
||||
if (!matcher.matches()) {
|
||||
@@ -167,14 +233,46 @@ data class Boost(
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
if (s.isNullOrEmpty()) return
|
||||
|
||||
val hasPrefix = s.startsWith(prefix)
|
||||
if (hasPrefix && s.length == prefix.length) {
|
||||
val hasSymbol = s.startsWith(symbol) || s.endsWith(symbol)
|
||||
if (hasSymbol && symbolPattern.matchEntire(s.toString()) != null) {
|
||||
s.clear()
|
||||
} else if (!hasPrefix) {
|
||||
s.insert(0, prefix)
|
||||
} else if (!hasSymbol) {
|
||||
val formatter = NumberFormat.getCurrencyInstance()
|
||||
formatter.currency = currency
|
||||
formatter.minimumFractionDigits = 0
|
||||
formatter.maximumFractionDigits = currency.defaultFractionDigits
|
||||
|
||||
val value = s.toString().toDoubleOrNull()
|
||||
|
||||
if (value != null) {
|
||||
val formatted = formatter.format(value)
|
||||
|
||||
text?.removeTextChangedListener(this)
|
||||
|
||||
s.replace(0, s.length, formatted)
|
||||
if (formatted.endsWith(symbol)) {
|
||||
val result: MatchResult? = symbolPattern.find(formatted)
|
||||
if (result != null && result.range.first < s.length) {
|
||||
text?.setSelection(result.range.first)
|
||||
}
|
||||
}
|
||||
|
||||
text?.addTextChangedListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
onCustomAmountChanged(s.removePrefix(prefix).toString())
|
||||
val withoutSymbol = s.removePrefix(symbol).removeSuffix(symbol).trim().toString()
|
||||
val withoutLeadingZeroes = withoutSymbol.replace(leadingZeroesPattern, "")
|
||||
if (withoutSymbol != withoutLeadingZeroes) {
|
||||
text?.removeTextChangedListener(this)
|
||||
|
||||
val start = s.indexOf(withoutSymbol)
|
||||
s.replace(start, start + withoutSymbol.length, withoutLeadingZeroes)
|
||||
|
||||
text?.addTextChangedListener(this)
|
||||
}
|
||||
|
||||
onCustomAmountChanged(s.removePrefix(symbol).removeSuffix(symbol).trim().toString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +280,7 @@ data class Boost(
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(SelectionModel::class.java, MappingAdapter.LayoutFactory({ SelectionViewHolder(it) }, R.layout.boost_preference))
|
||||
adapter.registerFactory(HeadingModel::class.java, MappingAdapter.LayoutFactory({ HeadingViewHolder(it) }, R.layout.boost_preview_preference))
|
||||
adapter.registerFactory(LoadingModel::class.java, MappingAdapter.LayoutFactory({ LoadingViewHolder(it) }, R.layout.boost_loading_preference))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import android.animation.Animator
|
||||
import android.view.View
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.airbnb.lottie.LottieDrawable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
/**
|
||||
* A simple mapping model to show a boost animation.
|
||||
*/
|
||||
object BoostAnimation {
|
||||
|
||||
class Model : PreferenceModel<Model>(isEnabled = true) {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val lottie: LottieAnimationView = findViewById(R.id.boost_animation_view)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
lottie.playAnimation()
|
||||
lottie.addAnimatorListener(object : AnimationCompleteListener() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
lottie.removeAnimatorListener(this)
|
||||
lottie.setMinAndMaxFrame(30, 91)
|
||||
lottie.repeatMode = LottieDrawable.RESTART
|
||||
lottie.repeatCount = LottieDrawable.INFINITE
|
||||
lottie.frame = 30
|
||||
lottie.playAnimation()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.boost_animation_pref))
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
|
||||
@@ -19,10 +23,18 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationExceptions
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.Progress
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyboard.findListener
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil.requireCoordinatorLayout
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
|
||||
/**
|
||||
@@ -32,32 +44,72 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
layoutId = R.layout.boost_bottom_sheet
|
||||
) {
|
||||
|
||||
private val viewModel: BoostViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
private val viewModel: BoostViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
BoostViewModel.Factory(BoostRepository(ApplicationDependencies.getDonationsService()), donationPaymentComponent.donationPaymentRepository, FETCH_BOOST_TOKEN_REQUEST_CODE)
|
||||
}
|
||||
)
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private lateinit var boost1AnimationView: LottieAnimationView
|
||||
private lateinit var boost2AnimationView: LottieAnimationView
|
||||
private lateinit var boost3AnimationView: LottieAnimationView
|
||||
private lateinit var boost4AnimationView: LottieAnimationView
|
||||
private lateinit var boost5AnimationView: LottieAnimationView
|
||||
private lateinit var boost6AnimationView: LottieAnimationView
|
||||
|
||||
private lateinit var processingDonationPaymentDialog: AlertDialog
|
||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
||||
|
||||
private val sayThanks: CharSequence by lazy {
|
||||
SpannableStringBuilder(requireContext().getString(R.string.BoostFragment__say_thanks_and_earn, 30))
|
||||
.append(" ")
|
||||
.append(
|
||||
SpanUtil.learnMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)) {
|
||||
// TODO [alex] -- Where's this go?
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.sustainer_boost_and_badges))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
donationPaymentComponent = findListener()!!
|
||||
viewModel.refresh()
|
||||
|
||||
CurrencySelection.register(adapter)
|
||||
BadgePreview.register(adapter)
|
||||
Boost.register(adapter)
|
||||
GooglePayButton.register(adapter)
|
||||
Progress.register(adapter)
|
||||
NetworkFailure.register(adapter)
|
||||
BoostAnimation.register(adapter)
|
||||
|
||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.processing_payment_dialog)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
|
||||
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
|
||||
|
||||
boost1AnimationView = requireView().findViewById(R.id.boost1_animation)
|
||||
boost2AnimationView = requireView().findViewById(R.id.boost2_animation)
|
||||
boost3AnimationView = requireView().findViewById(R.id.boost3_animation)
|
||||
boost4AnimationView = requireView().findViewById(R.id.boost4_animation)
|
||||
boost5AnimationView = requireView().findViewById(R.id.boost5_animation)
|
||||
boost6AnimationView = requireView().findViewById(R.id.boost6_animation)
|
||||
|
||||
KeyboardAwareLinearLayout(requireContext()).apply {
|
||||
addOnKeyboardHiddenListener {
|
||||
recyclerView.post { recyclerView.requestLayout() }
|
||||
}
|
||||
|
||||
addOnKeyboardShownListener {
|
||||
recyclerView.post { recyclerView.scrollToPosition(adapter.itemCount - 1) }
|
||||
}
|
||||
|
||||
requireCoordinatorLayout().addView(this)
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
@@ -65,15 +117,23 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
lifecycleDisposable += viewModel.events.subscribe { event: DonationEvent ->
|
||||
when (event) {
|
||||
is DonationEvent.GooglePayUnavailableError -> onGooglePayUnavailable(event.throwable)
|
||||
is DonationEvent.GooglePayUnavailableError -> Unit
|
||||
is DonationEvent.PaymentConfirmationError -> onPaymentError(event.throwable)
|
||||
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed(event.badge)
|
||||
DonationEvent.RequestTokenError -> onPaymentError(null)
|
||||
is DonationEvent.RequestTokenError -> onPaymentError(DonationExceptions.SetupFailed(event.throwable))
|
||||
DonationEvent.RequestTokenSuccess -> Log.i(TAG, "Successfully got request token from Google Pay")
|
||||
DonationEvent.SubscriptionCancelled -> Unit
|
||||
is DonationEvent.SubscriptionCancellationFailed -> Unit
|
||||
}
|
||||
}
|
||||
lifecycleDisposable += donationPaymentComponent.googlePayResultPublisher.subscribe {
|
||||
viewModel.onActivityResult(it.requestCode, it.resultCode, it.data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
processingDonationPaymentDialog.hide()
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: BoostState): DSLConfiguration {
|
||||
@@ -84,7 +144,7 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
}
|
||||
|
||||
return configure {
|
||||
customPref(BadgePreview.SubscriptionModel(state.boostBadge))
|
||||
customPref(BoostAnimation.Model())
|
||||
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(
|
||||
@@ -104,49 +164,62 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
|
||||
customPref(
|
||||
CurrencySelection.Model(
|
||||
currencySelection = state.currencySelection,
|
||||
selectedCurrency = state.currencySelection,
|
||||
isEnabled = state.stage == BoostState.Stage.READY,
|
||||
onClick = {
|
||||
findNavController().navigate(BoostFragmentDirections.actionBoostFragmentToSetDonationCurrencyFragment(true))
|
||||
findNavController().navigate(BoostFragmentDirections.actionBoostFragmentToSetDonationCurrencyFragment(true, viewModel.getSupportedCurrencyCodes().toTypedArray()))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
customPref(
|
||||
Boost.SelectionModel(
|
||||
boosts = state.boosts,
|
||||
selectedBoost = state.selectedBoost,
|
||||
currency = state.customAmount.currency,
|
||||
isCustomAmountFocused = state.isCustomAmountFocused,
|
||||
isEnabled = state.stage == BoostState.Stage.READY,
|
||||
onBoostClick = {
|
||||
viewModel.setSelectedBoost(it)
|
||||
},
|
||||
onCustomAmountChanged = {
|
||||
viewModel.setCustomAmount(it)
|
||||
},
|
||||
onCustomAmountFocusChanged = {
|
||||
viewModel.setCustomAmountFocused(it)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (state.isGooglePayAvailable) {
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (state.stage == BoostState.Stage.INIT) {
|
||||
customPref(
|
||||
GooglePayButton.Model(
|
||||
onClick = this@BoostFragment::onGooglePayButtonClicked,
|
||||
isEnabled = state.stage == BoostState.Stage.READY
|
||||
Boost.LoadingModel()
|
||||
)
|
||||
} else if (state.stage == BoostState.Stage.FAILURE) {
|
||||
space(DimensionUnit.DP.toPixels(20f).toInt())
|
||||
customPref(
|
||||
NetworkFailure.Model {
|
||||
viewModel.retry()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
customPref(
|
||||
Boost.SelectionModel(
|
||||
boosts = state.boosts,
|
||||
selectedBoost = state.selectedBoost,
|
||||
currency = state.customAmount.currency,
|
||||
isCustomAmountFocused = state.isCustomAmountFocused,
|
||||
isEnabled = state.stage == BoostState.Stage.READY,
|
||||
onBoostClick = { view, boost ->
|
||||
startAnimationAboveSelectedBoost(view)
|
||||
viewModel.setSelectedBoost(boost)
|
||||
},
|
||||
onCustomAmountChanged = {
|
||||
viewModel.setCustomAmount(it)
|
||||
},
|
||||
onCustomAmountFocusChanged = {
|
||||
viewModel.setCustomAmountFocused(it)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
|
||||
customPref(
|
||||
GooglePayButton.Model(
|
||||
onClick = this@BoostFragment::onGooglePayButtonClicked,
|
||||
isEnabled = state.stage == BoostState.Stage.READY
|
||||
)
|
||||
)
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(R.string.SubscribeFragment__more_payment_options),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_open_20, R.color.signal_accent_primary),
|
||||
onClick = {
|
||||
// TODO
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -165,38 +238,69 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
|
||||
|
||||
private fun onPaymentError(throwable: Throwable?) {
|
||||
if (throwable is DonationExceptions.TimedOutWaitingForTokenRedemption) {
|
||||
Log.w(TAG, "Error occurred while redeeming token", throwable)
|
||||
Log.w(TAG, "Timed out while redeeming token", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__redemption_still_pending)
|
||||
.setMessage(R.string.DonationsErrors__you_might_not_see_your_badge_right_away)
|
||||
.setTitle(R.string.DonationsErrors__still_processing)
|
||||
.setMessage(R.string.DonationsErrors__your_payment_is_still)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Error occurred while processing payment", throwable)
|
||||
.show()
|
||||
} else if (throwable is DonationExceptions.SetupFailed) {
|
||||
Log.w(TAG, "Error occurred while processing payment", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__payment_failed)
|
||||
.setTitle(R.string.DonationsErrors__error_processing_payment)
|
||||
.setMessage(R.string.DonationsErrors__your_payment)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
Log.w(TAG, "Error occurred while trying to redeem token", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__couldnt_add_badge)
|
||||
.setMessage(R.string.DonationsErrors__your_badge_could_not)
|
||||
.setPositiveButton(R.string.Subscription__contact_support) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGooglePayUnavailable(throwable: Throwable?) {
|
||||
Log.w(TAG, "Google Pay error", throwable)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__google_pay_unavailable)
|
||||
.setMessage(R.string.DonationsErrors__you_have_to_set_up_google_pay_to_donate_in_app)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
private fun startAnimationAboveSelectedBoost(view: View) {
|
||||
val animationView = getAnimationContainer(view)
|
||||
val viewProjection = Projection.relativeToViewRoot(view, null)
|
||||
val animationProjection = Projection.relativeToViewRoot(animationView, null)
|
||||
val viewHorizontalCenter = viewProjection.x + viewProjection.width / 2f
|
||||
val animationHorizontalCenter = animationProjection.x + animationProjection.width / 2f
|
||||
val animationBottom = animationProjection.y + animationProjection.height
|
||||
|
||||
animationView.translationY = -(animationBottom - viewProjection.y) + (viewProjection.height / 2f)
|
||||
animationView.translationX = viewHorizontalCenter - animationHorizontalCenter
|
||||
|
||||
animationView.playAnimation()
|
||||
|
||||
viewProjection.release()
|
||||
animationProjection.release()
|
||||
}
|
||||
|
||||
private fun getAnimationContainer(view: View): LottieAnimationView {
|
||||
return when (view.id) {
|
||||
R.id.boost_1 -> boost1AnimationView
|
||||
R.id.boost_2 -> boost2AnimationView
|
||||
R.id.boost_3 -> boost3AnimationView
|
||||
R.id.boost_4 -> boost4AnimationView
|
||||
R.id.boost_5 -> boost5AnimationView
|
||||
R.id.boost_6 -> boost6AnimationView
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BoostFragment::class.java)
|
||||
private const val FETCH_BOOST_TOKEN_REQUEST_CODE = 2000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,33 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import android.net.Uri
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
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 java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
class BoostRepository {
|
||||
class BoostRepository(private val donationsService: DonationsService) {
|
||||
|
||||
fun getBoosts(currency: Currency): Single<Pair<List<Boost>, Boost?>> {
|
||||
val boosts = testBoosts(currency)
|
||||
|
||||
return Single.just(
|
||||
Pair(
|
||||
boosts,
|
||||
boosts[2]
|
||||
)
|
||||
)
|
||||
fun getBoosts(): Single<Map<Currency, List<Boost>>> {
|
||||
return donationsService.boostAmounts
|
||||
.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)) } }
|
||||
}
|
||||
}
|
||||
|
||||
fun getBoostBadge(): Single<Badge> = Single.fromCallable {
|
||||
// Get boost badge from server
|
||||
// throw NotImplementedError()
|
||||
testBadge
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val testBadge = Badge(
|
||||
id = "TEST",
|
||||
category = Badge.Category.Testing,
|
||||
name = "Test Badge",
|
||||
description = "Test Badge",
|
||||
imageUrl = Uri.EMPTY,
|
||||
imageDensity = "xxxhdpi",
|
||||
expirationTimestamp = 0L,
|
||||
visible = false,
|
||||
)
|
||||
|
||||
private fun testBoosts(currency: Currency) = listOf(
|
||||
3L, 5L, 10L, 20L, 50L, 100L
|
||||
).map {
|
||||
Boost(testBadge, FiatMoney(BigDecimal.valueOf(it), currency))
|
||||
}
|
||||
fun getBoostBadge(): Single<Badge> {
|
||||
return donationsService.getBoostBadge(Locale.getDefault())
|
||||
.flatMap(ServiceResponse<SignalServiceProfile.Badge>::flattenResult)
|
||||
.map(Badges::fromServiceBadge)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,25 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.boost
|
||||
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
data class BoostState(
|
||||
val boostBadge: Badge? = null,
|
||||
val currencySelection: CurrencySelection = CurrencySelection("USD"),
|
||||
val currencySelection: Currency,
|
||||
val isGooglePayAvailable: Boolean = false,
|
||||
val boosts: List<Boost> = listOf(),
|
||||
val selectedBoost: Boost? = null,
|
||||
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, Currency.getInstance(currencySelection.selectedCurrencyCode)),
|
||||
val customAmount: FiatMoney = FiatMoney(BigDecimal.ZERO, currencySelection),
|
||||
val isCustomAmountFocused: Boolean = false,
|
||||
val stage: Stage = Stage.INIT,
|
||||
val supportedCurrencyCodes: List<String> = emptyList()
|
||||
) {
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
TOKEN_REQUEST,
|
||||
PAYMENT_PIPELINE,
|
||||
FAILURE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,25 @@ import com.google.android.gms.wallet.PaymentData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.InternetConnectionObserver
|
||||
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import java.lang.NumberFormatException
|
||||
import java.math.BigDecimal
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Currency
|
||||
|
||||
class BoostViewModel(
|
||||
private val boostRepository: BoostRepository,
|
||||
@@ -27,34 +34,78 @@ class BoostViewModel(
|
||||
private val fetchTokenRequestCode: Int
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(BoostState())
|
||||
private val store = Store(BoostState(currencySelection = SignalStore.donationsValues().getBoostCurrency()))
|
||||
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
|
||||
private val disposables = CompositeDisposable()
|
||||
private val networkDisposable: Disposable
|
||||
|
||||
val state: LiveData<BoostState> = store.stateLiveData
|
||||
val events: Observable<DonationEvent> = eventPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
private var boostToPurchase: Boost? = null
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
init {
|
||||
networkDisposable = InternetConnectionObserver
|
||||
.observe()
|
||||
.distinctUntilChanged()
|
||||
.subscribe { isConnected ->
|
||||
if (isConnected) {
|
||||
retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
override fun onCleared() {
|
||||
networkDisposable.dispose()
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun getSupportedCurrencyCodes(): List<String> {
|
||||
return store.state.supportedCurrencyCodes
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (!disposables.isDisposed && store.state.stage == BoostState.Stage.FAILURE) {
|
||||
store.update { it.copy(stage = BoostState.Stage.INIT) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
|
||||
val currencyObservable = SignalStore.donationsValues().observableBoostCurrency
|
||||
val boosts = currencyObservable.flatMapSingle { boostRepository.getBoosts(it) }
|
||||
val allBoosts = boostRepository.getBoosts()
|
||||
val boostBadge = boostRepository.getBoostBadge()
|
||||
|
||||
disposables += Observable.combineLatest(boosts, boostBadge.toObservable()) { (boosts, defaultBoost), badge -> BoostInfo(boosts, defaultBoost, badge) }.subscribe { info ->
|
||||
store.update {
|
||||
it.copy(
|
||||
boosts = info.boosts,
|
||||
selectedBoost = if (it.selectedBoost in info.boosts) it.selectedBoost else info.defaultBoost,
|
||||
boostBadge = it.boostBadge ?: info.boostBadge,
|
||||
stage = if (it.stage == BoostState.Stage.INIT) BoostState.Stage.READY else it.stage
|
||||
)
|
||||
disposables += Observable.combineLatest(currencyObservable, allBoosts.toObservable(), boostBadge.toObservable()) { currency, boostMap, badge ->
|
||||
val boostList = if (currency in boostMap) {
|
||||
boostMap[currency]!!
|
||||
} else {
|
||||
SignalStore.donationsValues().setBoostCurrency(PlatformCurrencyUtil.USD)
|
||||
listOf()
|
||||
}
|
||||
}
|
||||
|
||||
BoostInfo(boostList, boostList[2], badge, boostMap.keys)
|
||||
}.subscribeBy(
|
||||
onNext = { info ->
|
||||
store.update {
|
||||
it.copy(
|
||||
boosts = info.boosts,
|
||||
selectedBoost = if (it.selectedBoost in info.boosts) it.selectedBoost else info.defaultBoost,
|
||||
boostBadge = it.boostBadge ?: info.boostBadge,
|
||||
stage = if (it.stage == BoostState.Stage.INIT || it.stage == BoostState.Stage.FAILURE) BoostState.Stage.READY else it.stage,
|
||||
supportedCurrencyCodes = info.supportedCurrencies.map(Currency::getCurrencyCode)
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = { throwable ->
|
||||
Log.w(TAG, "Could not load boost information", throwable)
|
||||
store.update {
|
||||
it.copy(stage = BoostState.Stage.FAILURE)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
disposables += donationPaymentRepository.isGooglePayAvailable().subscribeBy(
|
||||
onComplete = { store.update { it.copy(isGooglePayAvailable = true) } },
|
||||
@@ -64,7 +115,7 @@ class BoostViewModel(
|
||||
disposables += currencyObservable.subscribeBy { currency ->
|
||||
store.update {
|
||||
it.copy(
|
||||
currencySelection = CurrencySelection(currency.currencyCode),
|
||||
currencySelection = currency,
|
||||
isCustomAmountFocused = false,
|
||||
customAmount = FiatMoney(
|
||||
BigDecimal.ZERO, currency
|
||||
@@ -79,25 +130,24 @@ class BoostViewModel(
|
||||
resultCode: Int,
|
||||
data: Intent?
|
||||
) {
|
||||
val boost = boostToPurchase
|
||||
boostToPurchase = null
|
||||
|
||||
donationPaymentRepository.onActivityResult(
|
||||
requestCode,
|
||||
resultCode,
|
||||
data,
|
||||
this.fetchTokenRequestCode,
|
||||
requestCode, resultCode, data, this.fetchTokenRequestCode,
|
||||
object : GooglePayApi.PaymentRequestCallback {
|
||||
override fun onSuccess(paymentData: PaymentData) {
|
||||
val boost = boostToPurchase
|
||||
boostToPurchase = null
|
||||
|
||||
if (boost != null) {
|
||||
eventPublisher.onNext(DonationEvent.RequestTokenSuccess)
|
||||
|
||||
store.update { it.copy(stage = BoostState.Stage.PAYMENT_PIPELINE) }
|
||||
|
||||
donationPaymentRepository.continuePayment(boost.price, paymentData).subscribeBy(
|
||||
onError = { throwable ->
|
||||
store.update { it.copy(stage = BoostState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.PaymentConfirmationError(throwable))
|
||||
},
|
||||
onComplete = {
|
||||
// TODO [alex] Now we need to do the whole query for a token, submit token rigamarole
|
||||
store.update { it.copy(stage = BoostState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.boostBadge!!))
|
||||
}
|
||||
@@ -107,9 +157,9 @@ class BoostViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError() {
|
||||
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
|
||||
store.update { it.copy(stage = BoostState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.RequestTokenError)
|
||||
eventPublisher.onNext(DonationEvent.RequestTokenError(googlePayException))
|
||||
}
|
||||
|
||||
override fun onCancelled() {
|
||||
@@ -125,17 +175,16 @@ class BoostViewModel(
|
||||
return
|
||||
}
|
||||
|
||||
store.update { it.copy(stage = BoostState.Stage.PAYMENT_PIPELINE) }
|
||||
store.update { it.copy(stage = BoostState.Stage.TOKEN_REQUEST) }
|
||||
|
||||
// TODO [alex] -- Do we want prevalidation? Stripe will catch us anyway.
|
||||
// TODO [alex] -- Custom boost badge details... how do we determine this?
|
||||
boostToPurchase = if (snapshot.isCustomAmountFocused) {
|
||||
Boost(snapshot.selectedBoost.badge, snapshot.customAmount)
|
||||
val boost = if (snapshot.isCustomAmountFocused) {
|
||||
Boost(snapshot.customAmount)
|
||||
} else {
|
||||
snapshot.selectedBoost
|
||||
}
|
||||
|
||||
donationPaymentRepository.requestTokenFromGooglePay(snapshot.selectedBoost.price, label, fetchTokenRequestCode)
|
||||
boostToPurchase = boost
|
||||
donationPaymentRepository.requestTokenFromGooglePay(boost.price, label, fetchTokenRequestCode)
|
||||
}
|
||||
|
||||
fun setSelectedBoost(boost: Boost) {
|
||||
@@ -148,10 +197,17 @@ class BoostViewModel(
|
||||
}
|
||||
|
||||
fun setCustomAmount(amount: String) {
|
||||
val bigDecimalAmount = if (amount.isEmpty()) {
|
||||
val bigDecimalAmount: BigDecimal = if (amount.isEmpty() || amount == DecimalFormatSymbols.getInstance().decimalSeparator.toString()) {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
BigDecimal(amount)
|
||||
val decimalFormat = DecimalFormat.getInstance() as DecimalFormat
|
||||
decimalFormat.isParseBigDecimal = true
|
||||
|
||||
try {
|
||||
decimalFormat.parse(amount) as BigDecimal
|
||||
} catch (e: NumberFormatException) {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
}
|
||||
|
||||
store.update { it.copy(customAmount = FiatMoney(bigDecimalAmount, it.customAmount.currency)) }
|
||||
@@ -161,7 +217,7 @@ class BoostViewModel(
|
||||
store.update { it.copy(isCustomAmountFocused = isFocused) }
|
||||
}
|
||||
|
||||
private data class BoostInfo(val boosts: List<Boost>, val defaultBoost: Boost?, val boostBadge: Badge)
|
||||
private data class BoostInfo(val boosts: List<Boost>, val defaultBoost: Boost?, val boostBadge: Badge, val supportedCurrencies: Set<Currency>)
|
||||
|
||||
class Factory(
|
||||
private val boostRepository: BoostRepository,
|
||||
@@ -172,4 +228,8 @@ class BoostViewModel(
|
||||
return modelClass.cast(BoostViewModel(boostRepository, donationPaymentRepository, fetchTokenRequestCode))!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BoostViewModel::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ 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.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.keyboard.findListener
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
@@ -13,13 +15,18 @@ import java.util.Locale
|
||||
*/
|
||||
class SetCurrencyFragment : DSLSettingsBottomSheetFragment() {
|
||||
|
||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
||||
|
||||
private val viewModel: SetCurrencyViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
SetCurrencyViewModel.Factory(SetCurrencyFragmentArgs.fromBundle(requireArguments()).isBoost)
|
||||
val args = SetCurrencyFragmentArgs.fromBundle(requireArguments())
|
||||
SetCurrencyViewModel.Factory(args.isBoost, args.supportedCurrencyCodes.toList())
|
||||
}
|
||||
)
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
donationPaymentComponent = findListener()!!
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
}
|
||||
@@ -28,12 +35,13 @@ class SetCurrencyFragment : DSLSettingsBottomSheetFragment() {
|
||||
private fun getConfiguration(state: SetCurrencyState): DSLConfiguration {
|
||||
return configure {
|
||||
state.currencies.forEach { currency ->
|
||||
radioPref(
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(currency.getDisplayName(Locale.getDefault())),
|
||||
summary = DSLSettingsText.from(currency.currencyCode),
|
||||
isChecked = currency.currencyCode == state.selectedCurrencyCode,
|
||||
onClick = {
|
||||
viewModel.setSelectedCurrency(currency.currencyCode)
|
||||
donationPaymentComponent.donationPaymentRepository.scheduleSyncForAccountRecordChange()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,42 +4,53 @@ import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
class SetCurrencyViewModel(val isBoost: Boolean) : ViewModel() {
|
||||
class SetCurrencyViewModel(
|
||||
private val isBoost: Boolean,
|
||||
supportedCurrencyCodes: List<String>
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(SetCurrencyState())
|
||||
private val store = Store(
|
||||
SetCurrencyState(
|
||||
selectedCurrencyCode = if (isBoost) {
|
||||
SignalStore.donationsValues().getBoostCurrency().currencyCode
|
||||
} else {
|
||||
SignalStore.donationsValues().getSubscriptionCurrency().currencyCode
|
||||
},
|
||||
currencies = supportedCurrencyCodes
|
||||
.map(Currency::getInstance)
|
||||
.sortedWith(CurrencyComparator(BuildConfig.DEFAULT_CURRENCIES.split(",")))
|
||||
)
|
||||
)
|
||||
|
||||
val state: LiveData<SetCurrencyState> = store.stateLiveData
|
||||
|
||||
init {
|
||||
val defaultCurrency = SignalStore.donationsValues().getSubscriptionCurrency()
|
||||
|
||||
store.update { state ->
|
||||
val platformCurrencies = Currency.getAvailableCurrencies()
|
||||
val stripeCurrencies = platformCurrencies
|
||||
.filter { StripeApi.Validation.supportedCurrencyCodes.contains(it.currencyCode) }
|
||||
.sortedWith(CurrencyComparator(BuildConfig.DEFAULT_CURRENCIES.split(",")))
|
||||
|
||||
state.copy(
|
||||
selectedCurrencyCode = defaultCurrency.currencyCode,
|
||||
currencies = stripeCurrencies
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedCurrency(selectedCurrencyCode: String) {
|
||||
store.update { it.copy(selectedCurrencyCode = selectedCurrencyCode) }
|
||||
|
||||
if (isBoost) {
|
||||
SignalStore.donationsValues().setBoostCurrency(Currency.getInstance(selectedCurrencyCode))
|
||||
} else {
|
||||
SignalStore.donationsValues().setSubscriptionCurrency(Currency.getInstance(selectedCurrencyCode))
|
||||
val currency = Currency.getInstance(selectedCurrencyCode)
|
||||
val subscriber = SignalStore.donationsValues().getSubscriber(currency)
|
||||
|
||||
if (subscriber != null) {
|
||||
SignalStore.donationsValues().setSubscriber(subscriber)
|
||||
} else {
|
||||
SignalStore.donationsValues().setSubscriber(
|
||||
Subscriber(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currencyCode = currency.currencyCode
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,9 +83,9 @@ class SetCurrencyViewModel(val isBoost: Boolean) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(private val isBoost: Boolean) : ViewModelProvider.Factory {
|
||||
class Factory(private val isBoost: Boolean, private val supportedCurrencyCodes: List<String>) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(SetCurrencyViewModel(isBoost))!!
|
||||
return modelClass.cast(SetCurrencyViewModel(isBoost, supportedCurrencyCodes))!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
@@ -20,9 +28,13 @@ import java.util.Locale
|
||||
object ActiveSubscriptionPreference {
|
||||
|
||||
class Model(
|
||||
val price: FiatMoney,
|
||||
val subscription: Subscription,
|
||||
val onAddBoostClick: () -> Unit,
|
||||
val renewalTimestamp: Long = -1L
|
||||
val renewalTimestamp: Long = -1L,
|
||||
val redemptionState: ManageDonationsState.SubscriptionRedemptionState,
|
||||
val activeSubscription: ActiveSubscription.Subscription,
|
||||
val onContactSupport: () -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean {
|
||||
return subscription.id == newItem.subscription.id
|
||||
@@ -31,7 +43,10 @@ object ActiveSubscriptionPreference {
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
subscription == newItem.subscription &&
|
||||
renewalTimestamp == newItem.renewalTimestamp
|
||||
renewalTimestamp == newItem.renewalTimestamp &&
|
||||
redemptionState == newItem.redemptionState &&
|
||||
FiatMoney.equals(price, newItem.price) &&
|
||||
activeSubscription == newItem.activeSubscription
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,32 +57,88 @@ object ActiveSubscriptionPreference {
|
||||
val price: TextView = itemView.findViewById(R.id.my_support_price)
|
||||
val expiry: TextView = itemView.findViewById(R.id.my_support_expiry)
|
||||
val boost: MaterialButton = itemView.findViewById(R.id.my_support_boost)
|
||||
val progress: ProgressBar = itemView.findViewById(R.id.my_support_progress)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
badge.setBadge(model.subscription.badge)
|
||||
title.text = model.subscription.title
|
||||
title.text = model.subscription.name
|
||||
|
||||
price.text = context.getString(
|
||||
R.string.MySupportPreference__s_per_month,
|
||||
FiatMoneyUtil.format(
|
||||
context.resources,
|
||||
model.subscription.price,
|
||||
model.price,
|
||||
FiatMoneyUtil.formatOptions()
|
||||
)
|
||||
)
|
||||
expiry.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
expiry.text = context.getString(
|
||||
R.string.MySupportPreference__renews_s,
|
||||
DateUtils.formatDate(
|
||||
Locale.getDefault(),
|
||||
model.renewalTimestamp
|
||||
)
|
||||
)
|
||||
when (model.redemptionState) {
|
||||
ManageDonationsState.SubscriptionRedemptionState.NONE -> presentRenewalState(model)
|
||||
ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS -> presentInProgressState()
|
||||
ManageDonationsState.SubscriptionRedemptionState.FAILED -> presentFailureState(model)
|
||||
}
|
||||
|
||||
boost.setOnClickListener {
|
||||
model.onAddBoostClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentRenewalState(model: Model) {
|
||||
expiry.text = context.getString(
|
||||
R.string.MySupportPreference__renews_s,
|
||||
DateUtils.formatDateWithYear(
|
||||
Locale.getDefault(),
|
||||
model.renewalTimestamp
|
||||
)
|
||||
)
|
||||
badge.alpha = 1f
|
||||
progress.visible = false
|
||||
}
|
||||
|
||||
private fun presentInProgressState() {
|
||||
expiry.text = context.getString(R.string.MySupportPreference__processing_transaction)
|
||||
badge.alpha = 0.2f
|
||||
progress.visible = true
|
||||
}
|
||||
|
||||
private fun presentFailureState(model: Model) {
|
||||
if (model.activeSubscription.isFailedPayment || SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt) {
|
||||
presentPaymentFailureState(model)
|
||||
} else {
|
||||
presentRedemptionFailureState(model)
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentPaymentFailureState(model: Model) {
|
||||
val contactString = context.getString(R.string.MySupportPreference__please_contact_support)
|
||||
|
||||
expiry.text = SpanUtil.clickSubstring(
|
||||
context.getString(R.string.DonationsErrors__error_processing_payment_s, contactString),
|
||||
contactString,
|
||||
{
|
||||
model.onContactSupport()
|
||||
},
|
||||
ContextCompat.getColor(context, R.color.signal_accent_primary)
|
||||
)
|
||||
badge.alpha = 0.2f
|
||||
progress.visible = false
|
||||
}
|
||||
|
||||
private fun presentRedemptionFailureState(model: Model) {
|
||||
val contactString = context.getString(R.string.MySupportPreference__please_contact_support)
|
||||
|
||||
expiry.text = SpanUtil.clickSubstring(
|
||||
context.getString(R.string.MySupportPreference__couldnt_add_badge_s, contactString),
|
||||
contactString,
|
||||
{
|
||||
model.onContactSupport()
|
||||
},
|
||||
ContextCompat.getColor(context, R.color.signal_accent_primary)
|
||||
)
|
||||
badge.alpha = 0.2f
|
||||
progress.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import java.util.Currency
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@@ -34,31 +37,15 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val args = ManageDonationsFragmentArgs.fromBundle(requireArguments())
|
||||
if (args.skipToSubscribe) {
|
||||
findNavController().navigate(
|
||||
ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscribeFragment(),
|
||||
NavOptions.Builder().setPopUpTo(R.id.manageDonationsFragment, true).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
val args = ManageDonationsFragmentArgs.fromBundle(requireArguments())
|
||||
if (args.skipToSubscribe) {
|
||||
return
|
||||
}
|
||||
|
||||
ActiveSubscriptionPreference.register(adapter)
|
||||
IndeterminateLoadingCircle.register(adapter)
|
||||
BadgePreview.register(adapter)
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
adapter.submitList(getConfiguration(state).toMappingModelList())
|
||||
@@ -75,6 +62,14 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||
|
||||
private fun getConfiguration(state: ManageDonationsState): DSLConfiguration {
|
||||
return configure {
|
||||
customPref(
|
||||
BadgePreview.Model(
|
||||
badge = state.featuredBadge
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||
|
||||
sectionHeaderPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.SubscribeFragment__signal_is_powered_by_people_like_you,
|
||||
@@ -82,27 +77,39 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||
)
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(32f).toInt())
|
||||
|
||||
noPadTextPref(
|
||||
title = DSLSettingsText.from(
|
||||
R.string.ManageDonationsFragment__my_support,
|
||||
DSLSettingsText.Title2BoldModifier
|
||||
DSLSettingsText.Body1BoldModifier, DSLSettingsText.BoldModifier
|
||||
)
|
||||
)
|
||||
|
||||
if (state.transactionState is ManageDonationsState.TransactionState.NotInTransaction) {
|
||||
val activeSubscription = state.transactionState.activeSubscription
|
||||
if (activeSubscription.isActive) {
|
||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { activeSubscription.activeSubscription.level == it.level }
|
||||
val activeSubscription = state.transactionState.activeSubscription.activeSubscription
|
||||
if (activeSubscription != null) {
|
||||
val subscription: Subscription? = state.availableSubscriptions.firstOrNull { activeSubscription.level == it.level }
|
||||
if (subscription != null) {
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
space(DimensionUnit.DP.toPixels(12f).toInt())
|
||||
|
||||
val activeCurrency = Currency.getInstance(activeSubscription.currency)
|
||||
val activeAmount = activeSubscription.amount.movePointLeft(activeCurrency.defaultFractionDigits)
|
||||
|
||||
customPref(
|
||||
ActiveSubscriptionPreference.Model(
|
||||
price = FiatMoney(activeAmount, activeCurrency),
|
||||
subscription = subscription,
|
||||
onAddBoostClick = {
|
||||
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToBoosts())
|
||||
},
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(activeSubscription.activeSubscription.endOfCurrentPeriod)
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(activeSubscription.endOfCurrentPeriod),
|
||||
redemptionState = state.getRedemptionState(),
|
||||
onContactSupport = {
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
},
|
||||
activeSubscription = activeSubscription
|
||||
)
|
||||
)
|
||||
|
||||
@@ -120,6 +127,7 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__manage_subscription),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_person_white_24dp),
|
||||
isEnabled = state.getRedemptionState() != ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS,
|
||||
onClick = {
|
||||
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscribeFragment())
|
||||
}
|
||||
@@ -129,7 +137,7 @@ class ManageDonationsFragment : DSLSettingsFragment() {
|
||||
title = DSLSettingsText.from(R.string.ManageDonationsFragment__badges),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_badge_24),
|
||||
onClick = {
|
||||
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscriptionBadgeManageFragment())
|
||||
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToManageBadges())
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -7,11 +7,35 @@ import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
data class ManageDonationsState(
|
||||
val featuredBadge: Badge? = null,
|
||||
val transactionState: TransactionState = TransactionState.Init,
|
||||
val availableSubscriptions: List<Subscription> = emptyList()
|
||||
val availableSubscriptions: List<Subscription> = emptyList(),
|
||||
private val subscriptionRedemptionState: SubscriptionRedemptionState = SubscriptionRedemptionState.NONE
|
||||
) {
|
||||
|
||||
fun getRedemptionState(): SubscriptionRedemptionState {
|
||||
return when (transactionState) {
|
||||
TransactionState.Init -> subscriptionRedemptionState
|
||||
TransactionState.InTransaction -> SubscriptionRedemptionState.IN_PROGRESS
|
||||
is TransactionState.NotInTransaction -> getStateFromActiveSubscription(transactionState.activeSubscription) ?: subscriptionRedemptionState
|
||||
}
|
||||
}
|
||||
|
||||
fun getStateFromActiveSubscription(activeSubscription: ActiveSubscription): SubscriptionRedemptionState? {
|
||||
return when {
|
||||
activeSubscription.isFailedPayment -> SubscriptionRedemptionState.FAILED
|
||||
activeSubscription.isInProgress -> SubscriptionRedemptionState.IN_PROGRESS
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TransactionState {
|
||||
object Init : TransactionState()
|
||||
object InTransaction : TransactionState()
|
||||
class NotInTransaction(val activeSubscription: ActiveSubscription) : TransactionState()
|
||||
}
|
||||
|
||||
enum class SubscriptionRedemptionState {
|
||||
NONE,
|
||||
IN_PROGRESS,
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,12 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
|
||||
class ManageDonationsViewModel(
|
||||
@@ -42,11 +42,27 @@ class ManageDonationsViewModel(
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
|
||||
val levelUpdateOperationEdges: Observable<Optional<LevelUpdateOperation>> = SignalStore.donationsValues().levelUpdateOperationObservable.distinctUntilChanged()
|
||||
val levelUpdateOperationEdges: Observable<Boolean> = LevelUpdate.isProcessing.distinctUntilChanged()
|
||||
val activeSubscription: Single<ActiveSubscription> = subscriptionsRepository.getActiveSubscription()
|
||||
|
||||
disposables += levelUpdateOperationEdges.flatMapSingle { optionalKey ->
|
||||
if (optionalKey.isPresent) {
|
||||
disposables += SubscriptionRedemptionJobWatcher.watch().subscribeBy { jobStateOptional ->
|
||||
store.update { manageDonationsState ->
|
||||
manageDonationsState.copy(
|
||||
subscriptionRedemptionState = jobStateOptional.transform { jobState ->
|
||||
when (jobState) {
|
||||
JobTracker.JobState.PENDING -> ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS
|
||||
JobTracker.JobState.RUNNING -> ManageDonationsState.SubscriptionRedemptionState.IN_PROGRESS
|
||||
JobTracker.JobState.SUCCESS -> ManageDonationsState.SubscriptionRedemptionState.NONE
|
||||
JobTracker.JobState.FAILURE -> ManageDonationsState.SubscriptionRedemptionState.FAILED
|
||||
JobTracker.JobState.IGNORED -> ManageDonationsState.SubscriptionRedemptionState.NONE
|
||||
}
|
||||
}.or(ManageDonationsState.SubscriptionRedemptionState.NONE)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += levelUpdateOperationEdges.flatMapSingle { isProcessing ->
|
||||
if (isProcessing) {
|
||||
Single.just(ManageDonationsState.TransactionState.InTransaction)
|
||||
} else {
|
||||
activeSubscription.map { ManageDonationsState.TransactionState.NotInTransaction(it) }
|
||||
@@ -57,7 +73,7 @@ class ManageDonationsViewModel(
|
||||
it.copy(transactionState = transactionState)
|
||||
}
|
||||
|
||||
if (transactionState is ManageDonationsState.TransactionState.NotInTransaction && !transactionState.activeSubscription.isActive) {
|
||||
if (transactionState is ManageDonationsState.TransactionState.NotInTransaction && transactionState.activeSubscription.activeSubscription == null) {
|
||||
eventPublisher.onNext(ManageDonationsEvent.NOT_SUBSCRIBED)
|
||||
}
|
||||
},
|
||||
@@ -66,9 +82,14 @@ class ManageDonationsViewModel(
|
||||
}
|
||||
)
|
||||
|
||||
disposables += subscriptionsRepository.getSubscriptions(SignalStore.donationsValues().getSubscriptionCurrency()).subscribeBy { subs ->
|
||||
store.update { it.copy(availableSubscriptions = subs) }
|
||||
}
|
||||
disposables += subscriptionsRepository.getSubscriptions().subscribeBy(
|
||||
onSuccess = { subs ->
|
||||
store.update { it.copy(availableSubscriptions = subs) }
|
||||
},
|
||||
onError = {
|
||||
Log.w(TAG, "Error retrieving subscriptions data", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
@@ -78,4 +99,8 @@ class ManageDonationsViewModel(
|
||||
return modelClass.cast(ManageDonationsViewModel(subscriptionsRepository))!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ManageDonationsViewModel::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.jobs.DonationReceiptRedemptionJob
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Allows observer to poll for the status of the latest pending, running, or completed redemption job for subscriptions.
|
||||
*/
|
||||
object SubscriptionRedemptionJobWatcher {
|
||||
fun watch(): Observable<Optional<JobTracker.JobState>> = Observable.interval(0, 5, TimeUnit.SECONDS).map {
|
||||
val redemptionJobState: JobTracker.JobState? = ApplicationDependencies.getJobManager().getFirstMatchingJobState {
|
||||
it.factoryKey == DonationReceiptRedemptionJob.KEY && it.parameters.queue == DonationReceiptRedemptionJob.SUBSCRIPTION_QUEUE
|
||||
}
|
||||
|
||||
val receiptJobState: JobTracker.JobState? = ApplicationDependencies.getJobManager().getFirstMatchingJobState {
|
||||
it.factoryKey == SubscriptionReceiptRequestResponseJob.KEY && it.parameters.queue == DonationReceiptRedemptionJob.SUBSCRIPTION_QUEUE
|
||||
}
|
||||
|
||||
val jobState: JobTracker.JobState? = redemptionJobState ?: receiptJobState
|
||||
|
||||
if (jobState == null && SignalStore.donationsValues().getSubscriptionRedemptionFailed()) {
|
||||
Optional.of(JobTracker.JobState.FAILURE)
|
||||
} else {
|
||||
Optional.fromNullable(jobState)
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
@@ -6,19 +6,16 @@ import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import java.util.Currency
|
||||
|
||||
data class CurrencySelection(
|
||||
val selectedCurrencyCode: String,
|
||||
) {
|
||||
object CurrencySelection {
|
||||
|
||||
companion object {
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.subscription_currency_selection))
|
||||
}
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.subscription_currency_selection))
|
||||
}
|
||||
|
||||
class Model(
|
||||
val currencySelection: CurrencySelection,
|
||||
val selectedCurrency: Currency,
|
||||
override val isEnabled: Boolean,
|
||||
val onClick: () -> Unit
|
||||
) : PreferenceModel<Model>(isEnabled = isEnabled) {
|
||||
@@ -28,7 +25,7 @@ data class CurrencySelection(
|
||||
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
newItem.currencySelection.selectedCurrencyCode == currencySelection.selectedCurrencyCode
|
||||
newItem.selectedCurrency == selectedCurrency
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +34,12 @@ data class CurrencySelection(
|
||||
private val spinner: TextView = itemView.findViewById(R.id.subscription_currency_selection_spinner)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
spinner.text = model.currencySelection.selectedCurrencyCode
|
||||
spinner.text = model.selectedCurrency.currencyCode
|
||||
|
||||
if (model.isEnabled) {
|
||||
itemView.setOnClickListener { model.onClick() }
|
||||
}
|
||||
itemView.setOnClickListener { model.onClick() }
|
||||
|
||||
itemView.isEnabled = model.isEnabled
|
||||
itemView.isClickable = model.isEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.models
|
||||
|
||||
import android.view.View
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
|
||||
/**
|
||||
* NetworkFailure will display a "card" to the user informing them that there
|
||||
* was a failure and give them a button which allows them to retry fetching data.
|
||||
*/
|
||||
object NetworkFailure {
|
||||
|
||||
class Model(
|
||||
val onRetryClick: () -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val retryButton = itemView.findViewById<MaterialButton>(R.id.retry_button)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
retryButton.setOnClickListener { model.onRetryClick() }
|
||||
}
|
||||
}
|
||||
|
||||
fun register(mappingAdapter: MappingAdapter) {
|
||||
mappingAdapter.registerFactory(Model::class.java, MappingAdapter.LayoutFactory({ ViewHolder(it) }, R.layout.network_failure_pref))
|
||||
}
|
||||
}
|
||||
@@ -10,26 +10,34 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.models.BadgePreview
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationExceptions
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.components.settings.models.Progress
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.help.HelpFragment
|
||||
import org.thoughtcrime.securesms.keyboard.findListener
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.Currency
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
@@ -39,21 +47,26 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
layoutId = R.layout.subscribe_fragment
|
||||
) {
|
||||
|
||||
private val viewModel: SubscribeViewModel by viewModels(ownerProducer = { requireActivity() })
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
private val supportTechSummary: CharSequence by lazy {
|
||||
SpannableStringBuilder(requireContext().getString(R.string.SubscribeFragment__support_technology_that_is_built_for_you))
|
||||
SpannableStringBuilder(requireContext().getString(R.string.SubscribeFragment__support_technology_that_is_built_for_you_not))
|
||||
.append(" ")
|
||||
.append(
|
||||
SpanUtil.learnMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)) {
|
||||
SpanUtil.readMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_button_secondary_text)) {
|
||||
findNavController().navigate(SubscribeFragmentDirections.actionSubscribeFragmentToSubscribeLearnMoreBottomSheetDialog())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private lateinit var processingDonationPaymentDialog: AlertDialog
|
||||
private lateinit var donationPaymentComponent: DonationPaymentComponent
|
||||
|
||||
private val viewModel: SubscribeViewModel by viewModels(
|
||||
factoryProducer = {
|
||||
SubscribeViewModel.Factory(SubscriptionsRepository(ApplicationDependencies.getDonationsService()), donationPaymentComponent.donationPaymentRepository, FETCH_SUBSCRIPTION_TOKEN_REQUEST_CODE)
|
||||
}
|
||||
)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
@@ -61,10 +74,15 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
override fun bindAdapter(adapter: DSLSettingsAdapter) {
|
||||
donationPaymentComponent = findListener()!!
|
||||
viewModel.refresh()
|
||||
|
||||
BadgePreview.register(adapter)
|
||||
CurrencySelection.register(adapter)
|
||||
Subscription.register(adapter)
|
||||
GooglePayButton.register(adapter)
|
||||
Progress.register(adapter)
|
||||
NetworkFailure.register(adapter)
|
||||
|
||||
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(R.layout.processing_payment_dialog)
|
||||
@@ -78,15 +96,23 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner.lifecycle)
|
||||
lifecycleDisposable += viewModel.events.subscribe {
|
||||
when (it) {
|
||||
is DonationEvent.GooglePayUnavailableError -> onGooglePayUnavailable(it.throwable)
|
||||
is DonationEvent.GooglePayUnavailableError -> Unit
|
||||
is DonationEvent.PaymentConfirmationError -> onPaymentError(it.throwable)
|
||||
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed(it.badge)
|
||||
DonationEvent.RequestTokenError -> onPaymentError(null)
|
||||
is DonationEvent.RequestTokenError -> onPaymentError(DonationExceptions.SetupFailed(it.throwable))
|
||||
DonationEvent.RequestTokenSuccess -> Log.w(TAG, "Successfully got request token from Google Pay")
|
||||
DonationEvent.SubscriptionCancelled -> onSubscriptionCancelled()
|
||||
is DonationEvent.SubscriptionCancellationFailed -> onSubscriptionFailedToCancel(it.throwable)
|
||||
}
|
||||
}
|
||||
lifecycleDisposable += donationPaymentComponent.googlePayResultPublisher.subscribe {
|
||||
viewModel.onActivityResult(it.requestCode, it.resultCode, it.data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
processingDonationPaymentDialog.hide()
|
||||
}
|
||||
|
||||
private fun getConfiguration(state: SubscribeState): DSLConfiguration {
|
||||
@@ -116,27 +142,58 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
|
||||
customPref(
|
||||
CurrencySelection.Model(
|
||||
currencySelection = state.currencySelection,
|
||||
selectedCurrency = state.currencySelection,
|
||||
isEnabled = areFieldsEnabled && state.activeSubscription?.isActive != true,
|
||||
onClick = {
|
||||
findNavController().navigate(SubscribeFragmentDirections.actionSubscribeFragmentToSetDonationCurrencyFragment(false))
|
||||
val selectableCurrencies = viewModel.getSelectableCurrencyCodes()
|
||||
if (selectableCurrencies != null) {
|
||||
findNavController().navigate(SubscribeFragmentDirections.actionSubscribeFragmentToSetDonationCurrencyFragment(false, selectableCurrencies.toTypedArray()))
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
state.subscriptions.forEach {
|
||||
val isActive = state.activeSubscription?.activeSubscription?.level == it.level
|
||||
space(DimensionUnit.DP.toPixels(4f).toInt())
|
||||
|
||||
@Suppress("CascadeIf")
|
||||
if (state.stage == SubscribeState.Stage.INIT) {
|
||||
customPref(
|
||||
Subscription.Model(
|
||||
subscription = it,
|
||||
isSelected = state.selectedSubscription == it,
|
||||
isEnabled = areFieldsEnabled,
|
||||
isActive = isActive,
|
||||
willRenew = isActive && state.activeSubscription?.activeSubscription?.willCancelAtPeriodEnd() ?: false,
|
||||
onClick = { viewModel.setSelectedSubscription(it) },
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(state.activeSubscription?.activeSubscription?.endOfCurrentPeriod ?: 0L)
|
||||
)
|
||||
Subscription.LoaderModel()
|
||||
)
|
||||
} else if (state.stage == SubscribeState.Stage.FAILURE) {
|
||||
space(DimensionUnit.DP.toPixels(69f).toInt())
|
||||
customPref(
|
||||
NetworkFailure.Model {
|
||||
viewModel.refresh()
|
||||
}
|
||||
)
|
||||
space(DimensionUnit.DP.toPixels(75f).toInt())
|
||||
} else {
|
||||
state.subscriptions.forEach {
|
||||
|
||||
val isActive = state.activeSubscription?.activeSubscription?.level == it.level && state.activeSubscription.isActive
|
||||
|
||||
val activePrice = state.activeSubscription?.activeSubscription?.let { sub ->
|
||||
val activeCurrency = Currency.getInstance(sub.currency)
|
||||
val activeAmount = sub.amount.movePointLeft(activeCurrency.defaultFractionDigits)
|
||||
|
||||
FiatMoney(activeAmount, activeCurrency)
|
||||
}
|
||||
|
||||
customPref(
|
||||
Subscription.Model(
|
||||
activePrice = if (isActive) activePrice else null,
|
||||
subscription = it,
|
||||
isSelected = state.selectedSubscription == it,
|
||||
isEnabled = areFieldsEnabled,
|
||||
isActive = isActive,
|
||||
willRenew = isActive && !state.isSubscriptionExpiring(),
|
||||
onClick = { viewModel.setSelectedSubscription(it) },
|
||||
renewalTimestamp = TimeUnit.SECONDS.toMillis(state.activeSubscription?.activeSubscription?.endOfCurrentPeriod ?: 0L),
|
||||
selectedCurrency = state.currencySelection
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.activeSubscription?.isActive == true) {
|
||||
@@ -144,20 +201,25 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
|
||||
val activeAndSameLevel = state.activeSubscription.isActive &&
|
||||
state.selectedSubscription?.level == state.activeSubscription.activeSubscription?.level
|
||||
val isExpiring = state.activeSubscription.isActive && state.activeSubscription.activeSubscription?.willCancelAtPeriodEnd() == true
|
||||
|
||||
primaryButton(
|
||||
text = DSLSettingsText.from(R.string.SubscribeFragment__update_subscription),
|
||||
isEnabled = areFieldsEnabled && (!activeAndSameLevel || isExpiring),
|
||||
isEnabled = areFieldsEnabled && (!activeAndSameLevel || state.isSubscriptionExpiring()),
|
||||
onClick = {
|
||||
val price = viewModel.getPriceOfSelectedSubscription() ?: return@primaryButton
|
||||
val calendar = Calendar.getInstance()
|
||||
|
||||
calendar.add(Calendar.MONTH, 1)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.SubscribeFragment__update_subscription_question)
|
||||
.setMessage(
|
||||
getString(
|
||||
R.string.SubscribeFragment__you_will_be_charged_the_full_amount,
|
||||
DateUtils.formatDateWithYear(Locale.getDefault(), calendar.timeInMillis)
|
||||
R.string.SubscribeFragment__you_will_be_charged_the_full_amount_s_of,
|
||||
FiatMoneyUtil.format(
|
||||
requireContext().resources,
|
||||
price,
|
||||
FiatMoneyUtil.formatOptions().trimZerosAfterDecimal()
|
||||
)
|
||||
)
|
||||
)
|
||||
.setPositiveButton(R.string.SubscribeFragment__update) { dialog, _ ->
|
||||
@@ -189,24 +251,16 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
}
|
||||
)
|
||||
} else {
|
||||
if (state.isGooglePayAvailable) {
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
space(DimensionUnit.DP.toPixels(16f).toInt())
|
||||
|
||||
customPref(
|
||||
GooglePayButton.Model(
|
||||
onClick = this@SubscribeFragment::onGooglePayButtonClicked,
|
||||
isEnabled = areFieldsEnabled && state.selectedSubscription != null
|
||||
)
|
||||
customPref(
|
||||
GooglePayButton.Model(
|
||||
onClick = this@SubscribeFragment::onGooglePayButtonClicked,
|
||||
isEnabled = areFieldsEnabled && state.selectedSubscription != null
|
||||
)
|
||||
}
|
||||
|
||||
secondaryButtonNoOutline(
|
||||
text = DSLSettingsText.from(R.string.SubscribeFragment__more_payment_options),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_open_20, R.color.signal_accent_primary),
|
||||
onClick = {
|
||||
// TODO [alex] support page
|
||||
}
|
||||
)
|
||||
|
||||
space(DimensionUnit.DP.toPixels(8f).toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,37 +277,50 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
|
||||
private fun onPaymentError(throwable: Throwable?) {
|
||||
if (throwable is DonationExceptions.TimedOutWaitingForTokenRedemption) {
|
||||
Log.w(TAG, "Error occurred while redeeming token", throwable)
|
||||
Log.w(TAG, "Timeout occurred while redeeming token", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__redemption_still_pending)
|
||||
.setMessage(R.string.DonationsErrors__you_might_not_see_your_badge_right_away)
|
||||
.setTitle(R.string.DonationsErrors__still_processing)
|
||||
.setMessage(R.string.DonationsErrors__your_payment_is_still)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||
requireActivity().startActivity(AppSettingsActivity.manageSubscriptions(requireContext()))
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Error occurred while processing payment", throwable)
|
||||
.show()
|
||||
} else if (throwable is DonationExceptions.SetupFailed) {
|
||||
Log.w(TAG, "Error occurred while processing payment", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__payment_failed)
|
||||
.setTitle(R.string.DonationsErrors__error_processing_payment)
|
||||
.setMessage(R.string.DonationsErrors__your_payment)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
} else if (SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt) {
|
||||
Log.w(TAG, "Stripe failed to process payment", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__error_processing_payment)
|
||||
.setMessage(R.string.DonationsErrors__your_badge_could_not_be_added)
|
||||
.setPositiveButton(R.string.Subscription__contact_support) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
Log.w(TAG, "Error occurred while trying to redeem token", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__couldnt_add_badge)
|
||||
.setMessage(R.string.DonationsErrors__your_badge_could_not)
|
||||
.setPositiveButton(R.string.Subscription__contact_support) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.DONATION_INDEX))
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onGooglePayUnavailable(throwable: Throwable?) {
|
||||
Log.w(TAG, "Google Pay error", throwable)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__google_pay_unavailable)
|
||||
.setMessage(R.string.DonationsErrors__you_have_to_set_up_google_pay_to_donate_in_app)
|
||||
.setPositiveButton(android.R.string.ok) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSubscriptionCancelled() {
|
||||
Snackbar.make(requireView(), R.string.SubscribeFragment__your_subscription_has_been_cancelled, Snackbar.LENGTH_LONG)
|
||||
.setTextColor(Color.WHITE)
|
||||
@@ -264,7 +331,7 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
private fun onSubscriptionFailedToCancel(throwable: Throwable) {
|
||||
Log.w(TAG, "Failed to cancel subscription", throwable)
|
||||
Log.w(TAG, "Failed to cancel subscription", throwable, true)
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.DonationsErrors__failed_to_cancel_subscription)
|
||||
.setMessage(R.string.DonationsErrors__subscription_cancellation_requires_an_internet_connection)
|
||||
@@ -272,9 +339,11 @@ class SubscribeFragment : DSLSettingsFragment(
|
||||
dialog.dismiss()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SubscribeFragment::class.java)
|
||||
private const val FETCH_SUBSCRIPTION_TOKEN_REQUEST_CODE = 1000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.subscribe
|
||||
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import java.util.Currency
|
||||
|
||||
data class SubscribeState(
|
||||
val currencySelection: CurrencySelection = CurrencySelection("USD"),
|
||||
val currencySelection: Currency,
|
||||
val subscriptions: List<Subscription> = listOf(),
|
||||
val selectedSubscription: Subscription? = null,
|
||||
val activeSubscription: ActiveSubscription? = null,
|
||||
@@ -13,11 +13,17 @@ data class SubscribeState(
|
||||
val stage: Stage = Stage.INIT,
|
||||
val hasInProgressSubscriptionTransaction: Boolean = false,
|
||||
) {
|
||||
|
||||
fun isSubscriptionExpiring(): Boolean {
|
||||
return activeSubscription?.isActive == true && activeSubscription.activeSubscription.willCancelAtPeriodEnd()
|
||||
}
|
||||
|
||||
enum class Stage {
|
||||
INIT,
|
||||
READY,
|
||||
TOKEN_REQUEST,
|
||||
PAYMENT_PIPELINE,
|
||||
CANCELLING
|
||||
CANCELLING,
|
||||
FAILURE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,30 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.gms.wallet.PaymentData
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.GooglePayApi
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationExceptions
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
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.livedata.Store
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.util.Currency
|
||||
|
||||
class SubscribeViewModel(
|
||||
@@ -28,9 +38,10 @@ class SubscribeViewModel(
|
||||
private val fetchTokenRequestCode: Int
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store(SubscribeState())
|
||||
private val store = Store(SubscribeState(currencySelection = SignalStore.donationsValues().getSubscriptionCurrency()))
|
||||
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
|
||||
private val disposables = CompositeDisposable()
|
||||
private val networkDisposable: Disposable
|
||||
|
||||
val state: LiveData<SubscribeState> = store.stateLiveData
|
||||
val events: Observable<DonationEvent> = eventPublisher.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -38,48 +49,109 @@ class SubscribeViewModel(
|
||||
private var subscriptionToPurchase: Subscription? = null
|
||||
private val activeSubscriptionSubject = PublishSubject.create<ActiveSubscription>()
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.clear()
|
||||
init {
|
||||
networkDisposable = InternetConnectionObserver
|
||||
.observe()
|
||||
.distinctUntilChanged()
|
||||
.subscribe { isConnected ->
|
||||
if (isConnected) {
|
||||
retry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
override fun onCleared() {
|
||||
networkDisposable.dispose()
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
fun getPriceOfSelectedSubscription(): FiatMoney? {
|
||||
return store.state.selectedSubscription?.prices?.first { it.currency == store.state.currencySelection }
|
||||
}
|
||||
|
||||
fun getSelectableCurrencyCodes(): List<String>? {
|
||||
return store.state.subscriptions.firstOrNull()?.prices?.map { it.currency.currencyCode }
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
if (!disposables.isDisposed && store.state.stage == SubscribeState.Stage.FAILURE) {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.INIT) }
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
disposables.clear()
|
||||
|
||||
val currency: Observable<Currency> = SignalStore.donationsValues().observableSubscriptionCurrency
|
||||
val allSubscriptions: Observable<List<Subscription>> = currency.switchMapSingle { subscriptionsRepository.getSubscriptions(it) }
|
||||
val allSubscriptions: Single<List<Subscription>> = subscriptionsRepository.getSubscriptions()
|
||||
|
||||
refreshActiveSubscription()
|
||||
|
||||
disposables += SignalStore.donationsValues().levelUpdateOperationObservable.subscribeBy {
|
||||
disposables += LevelUpdate.isProcessing.subscribeBy {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
hasInProgressSubscriptionTransaction = it.isPresent
|
||||
hasInProgressSubscriptionTransaction = it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
disposables += Observable.combineLatest(allSubscriptions, activeSubscriptionSubject, ::Pair).subscribe { (subs, active) ->
|
||||
store.update {
|
||||
it.copy(
|
||||
subscriptions = subs,
|
||||
selectedSubscription = it.selectedSubscription ?: resolveSelectedSubscription(active, subs),
|
||||
activeSubscription = active,
|
||||
stage = if (it.stage == SubscribeState.Stage.INIT) SubscribeState.Stage.READY else it.stage,
|
||||
)
|
||||
}
|
||||
}
|
||||
disposables += allSubscriptions.subscribeBy(
|
||||
onSuccess = { subscriptions ->
|
||||
if (subscriptions.isNotEmpty()) {
|
||||
val priceCurrencies = subscriptions[0].prices.map { it.currency }
|
||||
val selectedCurrency = SignalStore.donationsValues().getSubscriptionCurrency()
|
||||
|
||||
if (selectedCurrency !in priceCurrencies) {
|
||||
Log.w(TAG, "Unsupported currency selection. Defaulting to USD. $currency isn't supported.")
|
||||
val usd = PlatformCurrencyUtil.USD
|
||||
val newSubscriber = SignalStore.donationsValues().getSubscriber(usd) ?: Subscriber(SubscriberId.generate(), usd.currencyCode)
|
||||
SignalStore.donationsValues().setSubscriber(newSubscriber)
|
||||
donationPaymentRepository.scheduleSyncForAccountRecordChange()
|
||||
}
|
||||
}
|
||||
},
|
||||
onError = {}
|
||||
)
|
||||
|
||||
disposables += Observable.combineLatest(allSubscriptions.toObservable(), activeSubscriptionSubject, ::Pair).subscribeBy(
|
||||
onNext = { (subs, active) ->
|
||||
store.update {
|
||||
it.copy(
|
||||
subscriptions = subs,
|
||||
selectedSubscription = it.selectedSubscription ?: resolveSelectedSubscription(active, subs),
|
||||
activeSubscription = active,
|
||||
stage = if (it.stage == SubscribeState.Stage.INIT || it.stage == SubscribeState.Stage.FAILURE) SubscribeState.Stage.READY else it.stage,
|
||||
)
|
||||
}
|
||||
},
|
||||
onError = this::handleSubscriptionDataLoadFailure
|
||||
)
|
||||
|
||||
disposables += donationPaymentRepository.isGooglePayAvailable().subscribeBy(
|
||||
onComplete = { store.update { it.copy(isGooglePayAvailable = true) } },
|
||||
onError = { eventPublisher.onNext(DonationEvent.GooglePayUnavailableError(it)) }
|
||||
)
|
||||
|
||||
disposables += currency.map { CurrencySelection(it.currencyCode) }.subscribe { selection ->
|
||||
disposables += currency.subscribe { selection ->
|
||||
store.update { it.copy(currencySelection = selection) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSubscriptionDataLoadFailure(throwable: Throwable) {
|
||||
Log.w(TAG, "Could not load subscription data", throwable)
|
||||
store.update {
|
||||
it.copy(stage = SubscribeState.Stage.FAILURE)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshActiveSubscription() {
|
||||
subscriptionsRepository
|
||||
.getActiveSubscription()
|
||||
.subscribeBy { activeSubscriptionSubject.onNext(it) }
|
||||
.subscribeBy(
|
||||
onSuccess = { activeSubscriptionSubject.onNext(it) },
|
||||
onError = { activeSubscriptionSubject.onNext(ActiveSubscription(null)) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveSelectedSubscription(activeSubscription: ActiveSubscription, subscriptions: List<Subscription>): Subscription? {
|
||||
@@ -90,12 +162,28 @@ class SubscribeViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelActiveSubscriptionIfNecessary(): Completable {
|
||||
return Single.just(SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt).flatMapCompletable {
|
||||
if (it) {
|
||||
donationPaymentRepository.cancelActiveSubscription().doOnComplete {
|
||||
SignalStore.donationsValues().setLastEndOfPeriod(0L)
|
||||
SignalStore.donationsValues().clearLevelOperations()
|
||||
SignalStore.donationsValues().shouldCancelSubscriptionBeforeNextSubscribeAttempt = false
|
||||
}
|
||||
} else {
|
||||
Completable.complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.CANCELLING) }
|
||||
disposables += donationPaymentRepository.cancelActiveSubscription().subscribeBy(
|
||||
onComplete = {
|
||||
eventPublisher.onNext(DonationEvent.SubscriptionCancelled)
|
||||
SignalStore.donationsValues().setLastEndOfPeriod(0L)
|
||||
SignalStore.donationsValues().clearLevelOperations()
|
||||
SignalStore.donationsValues().markUserManuallyCancelled()
|
||||
refreshActiveSubscription()
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
},
|
||||
@@ -127,7 +215,12 @@ class SubscribeViewModel(
|
||||
|
||||
store.update { it.copy(stage = SubscribeState.Stage.PAYMENT_PIPELINE) }
|
||||
|
||||
ensureSubscriberId.andThen(continueSetup).andThen(setLevel).subscribeBy(
|
||||
val setup = ensureSubscriberId
|
||||
.andThen(cancelActiveSubscriptionIfNecessary())
|
||||
.andThen(continueSetup)
|
||||
.onErrorResumeNext { Completable.error(DonationExceptions.SetupFailed(it)) }
|
||||
|
||||
setup.andThen(setLevel).subscribeBy(
|
||||
onError = { throwable ->
|
||||
refreshActiveSubscription()
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
@@ -143,9 +236,9 @@ class SubscribeViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError() {
|
||||
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
eventPublisher.onNext(DonationEvent.RequestTokenError)
|
||||
eventPublisher.onNext(DonationEvent.RequestTokenError(googlePayException))
|
||||
}
|
||||
|
||||
override fun onCancelled() {
|
||||
@@ -157,7 +250,7 @@ class SubscribeViewModel(
|
||||
|
||||
fun updateSubscription() {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.PAYMENT_PIPELINE) }
|
||||
donationPaymentRepository.setSubscriptionLevel(store.state.selectedSubscription!!.level.toString())
|
||||
cancelActiveSubscriptionIfNecessary().andThen(donationPaymentRepository.setSubscriptionLevel(store.state.selectedSubscription!!.level.toString()))
|
||||
.subscribeBy(
|
||||
onComplete = {
|
||||
store.update { it.copy(stage = SubscribeState.Stage.READY) }
|
||||
@@ -178,8 +271,10 @@ class SubscribeViewModel(
|
||||
|
||||
store.update { it.copy(stage = SubscribeState.Stage.TOKEN_REQUEST) }
|
||||
|
||||
val selectedCurrency = snapshot.currencySelection
|
||||
|
||||
subscriptionToPurchase = snapshot.selectedSubscription
|
||||
donationPaymentRepository.requestTokenFromGooglePay(snapshot.selectedSubscription.price, snapshot.selectedSubscription.title, fetchTokenRequestCode)
|
||||
donationPaymentRepository.requestTokenFromGooglePay(snapshot.selectedSubscription.prices.first { it.currency == selectedCurrency }, snapshot.selectedSubscription.name, fetchTokenRequestCode)
|
||||
}
|
||||
|
||||
fun setSelectedSubscription(subscription: Subscription) {
|
||||
@@ -195,4 +290,8 @@ class SubscribeViewModel(
|
||||
return modelClass.cast(SubscribeViewModel(subscriptionsRepository, donationPaymentRepository, fetchTokenRequestCode))!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(SubscribeViewModel::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,28 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.thanks
|
||||
import android.animation.Animator
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.airbnb.lottie.LottieDrawable
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.badges.BadgeRepository
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
@@ -49,6 +55,7 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
|
||||
val done: MaterialButton = view.findViewById(R.id.thanks_bottom_sheet_done)
|
||||
val controlText: TextView = view.findViewById(R.id.thanks_bottom_sheet_control_text)
|
||||
val controlNote: View = view.findViewById(R.id.thanks_bottom_sheet_featured_note)
|
||||
val subhead: TextView = view.findViewById(R.id.thanks_bottom_sheet_subhead)
|
||||
|
||||
heading = view.findViewById(R.id.thanks_bottom_sheet_heading)
|
||||
switch = view.findViewById(R.id.thanks_bottom_sheet_switch)
|
||||
@@ -58,9 +65,31 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
|
||||
badgeView.setBadge(args.badge)
|
||||
badgeName.text = args.badge.name
|
||||
|
||||
if (args.badge.isBoost()) {
|
||||
if (Recipient.self().badges.any { !it.isBoost() }) {
|
||||
subhead.setText(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__youve_earned_a_boost_badge_help_signal)
|
||||
} else {
|
||||
subhead.text = SpannableStringBuilder(getString(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__youve_earned_a_boost_badge_help_signal))
|
||||
.append(" ")
|
||||
.append(getString(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__you_can_also))
|
||||
.append(" ")
|
||||
.append(
|
||||
SpanUtil.clickable(
|
||||
getString(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__become_a_montly_sustainer),
|
||||
ContextCompat.getColor(requireContext(), R.color.signal_accent_primary),
|
||||
) {
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
subhead.text = getString(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__youve_earned_s_badge_help_signal, args.badge.name)
|
||||
}
|
||||
|
||||
val otherBadges = Recipient.self().badges.filterNot { it.id == args.badge.id }
|
||||
val hasOtherBadges = otherBadges.isNotEmpty()
|
||||
val displayingBadges = otherBadges.all { it.visible }
|
||||
val displayingBadges = SignalStore.donationsValues().getDisplayBadgesOnProfile()
|
||||
|
||||
if (hasOtherBadges && displayingBadges) {
|
||||
switch.isChecked = false
|
||||
@@ -81,6 +110,7 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
|
||||
|
||||
if (args.isBoost) {
|
||||
presentBoostCopy()
|
||||
badgeView.visibility = View.INVISIBLE
|
||||
lottie.visible = true
|
||||
lottie.playAnimation()
|
||||
lottie.addAnimatorListener(object : AnimationCompleteListener() {
|
||||
@@ -106,16 +136,24 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
|
||||
val args = ThanksForYourSupportBottomSheetDialogFragmentArgs.fromBundle(requireArguments())
|
||||
|
||||
if (controlState == ControlState.DISPLAY) {
|
||||
badgeRepository.setVisibilityForAllBadges(controlChecked).subscribe()
|
||||
} else {
|
||||
badgeRepository.setFeaturedBadge(args.badge).subscribe()
|
||||
badgeRepository.setVisibilityForAllBadges(controlChecked).subscribeBy(
|
||||
onError = {
|
||||
Log.w(TAG, "Failure while updating badge visibility", it)
|
||||
}
|
||||
)
|
||||
} else if (controlChecked) {
|
||||
badgeRepository.setFeaturedBadge(args.badge).subscribeBy(
|
||||
onError = {
|
||||
Log.w(TAG, "Failure while updating featured badge", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (args.isBoost) {
|
||||
findNavController().popBackStack()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
|
||||
requireActivity().startActivity(AppSettingsActivity.manageSubscriptions(requireContext()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,4 +164,8 @@ class ThanksForYourSupportBottomSheetDialogFragment : FixedRoundedCornerBottomSh
|
||||
private fun presentSubscriptionCopy() {
|
||||
heading.setText(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__thanks_for_your_support)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ThanksForYourSupportBottomSheetDialogFragment::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@@ -30,6 +31,7 @@ import org.thoughtcrime.securesms.MuteDialog
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.badges.Badges.displayBadges
|
||||
import org.thoughtcrime.securesms.badges.models.Badge
|
||||
@@ -77,6 +79,7 @@ import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroup
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ContextUtil
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
|
||||
@@ -126,7 +129,9 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
private lateinit var callback: Callback
|
||||
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var toolbarAvatarContainer: FrameLayout
|
||||
private lateinit var toolbarAvatar: AvatarImageView
|
||||
private lateinit var toolbarBadge: BadgeImageView
|
||||
private lateinit var toolbarTitle: TextView
|
||||
private lateinit var toolbarBackground: View
|
||||
|
||||
@@ -140,7 +145,9 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
toolbar = view.findViewById(R.id.toolbar)
|
||||
toolbarAvatarContainer = view.findViewById(R.id.toolbar_avatar_container)
|
||||
toolbarAvatar = view.findViewById(R.id.toolbar_avatar)
|
||||
toolbarBadge = view.findViewById(R.id.toolbar_badge)
|
||||
toolbarTitle = view.findViewById(R.id.toolbar_title)
|
||||
toolbarBackground = view.findViewById(R.id.toolbar_background)
|
||||
|
||||
@@ -164,7 +171,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
override fun getOnScrollAnimationHelper(toolbarShadow: View): OnScrollAnimationHelper {
|
||||
return ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatar, toolbarTitle, toolbarBackground, toolbarShadow)
|
||||
return ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatarContainer, toolbarTitle, toolbarBackground, toolbarShadow)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
@@ -208,6 +215,10 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
.withFixedSize(ViewUtil.dpToPx(80))
|
||||
.load(state.recipient)
|
||||
|
||||
if (FeatureFlags.displayDonorBadges() && !state.recipient.isSelf) {
|
||||
toolbarBadge.setBadgeFromRecipient(state.recipient)
|
||||
}
|
||||
|
||||
state.withRecipientSettingsState {
|
||||
toolbarTitle.text = state.recipient.getDisplayName(requireContext())
|
||||
}
|
||||
@@ -488,6 +499,12 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
sectionHeaderPref(R.string.ManageProfileFragment_badges)
|
||||
|
||||
displayBadges(requireContext(), state.recipient.badges)
|
||||
|
||||
textPref(
|
||||
summary = DSLSettingsText.from(
|
||||
R.string.ConversationSettingsFragment__get_badges
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (recipientSettingsState.selfHasGroups) {
|
||||
@@ -715,7 +732,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
}
|
||||
|
||||
private fun showGroupInvitesSentDialog(showGroupInvitesSentDialog: ConversationSettingsEvent.ShowGroupInvitesSentDialog) {
|
||||
GroupInviteSentDialog.showInvitesSent(requireContext(), showGroupInvitesSentDialog.invitesSentTo)
|
||||
GroupInviteSentDialog.showInvitesSent(requireContext(), viewLifecycleOwner, showGroupInvitesSentDialog.invitesSentTo)
|
||||
}
|
||||
|
||||
private fun showMembersAdded(showMembersAdded: ConversationSettingsEvent.ShowMembersAdded) {
|
||||
|
||||
@@ -9,9 +9,9 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
@@ -40,27 +40,27 @@ class ConversationSettingsRepository(
|
||||
return if (threadId <= 0) {
|
||||
Optional.absent()
|
||||
} else {
|
||||
Optional.of(DatabaseFactory.getMediaDatabase(context).getGalleryMediaForThread(threadId, MediaDatabase.Sorting.Newest))
|
||||
Optional.of(SignalDatabase.media.getGalleryMediaForThread(threadId, MediaDatabase.Sorting.Newest))
|
||||
}
|
||||
}
|
||||
|
||||
fun getThreadId(recipientId: RecipientId, consumer: (Long) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipientId))
|
||||
consumer(SignalDatabase.threads.getThreadIdIfExistsFor(recipientId))
|
||||
}
|
||||
}
|
||||
|
||||
fun getThreadId(groupId: GroupId, consumer: (Long) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipientId = Recipient.externalGroupExact(context, groupId).id
|
||||
consumer(DatabaseFactory.getThreadDatabase(context).getThreadIdIfExistsFor(recipientId))
|
||||
consumer(SignalDatabase.threads.getThreadIdIfExistsFor(recipientId))
|
||||
}
|
||||
}
|
||||
|
||||
fun isInternalRecipientDetailsEnabled(): Boolean = SignalStore.internalValues().recipientDetails()
|
||||
|
||||
fun hasGroups(consumer: (Boolean) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute { consumer(DatabaseFactory.getGroupDatabase(context).activeGroupCount > 0) }
|
||||
SignalExecutors.BOUNDED.execute { consumer(SignalDatabase.groups.activeGroupCount > 0) }
|
||||
}
|
||||
|
||||
fun getIdentity(recipientId: RecipientId, consumer: (IdentityRecord?) -> Unit) {
|
||||
@@ -72,8 +72,8 @@ class ConversationSettingsRepository(
|
||||
fun getGroupsInCommon(recipientId: RecipientId, consumer: (List<Recipient>) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(
|
||||
DatabaseFactory
|
||||
.getGroupDatabase(context)
|
||||
SignalDatabase
|
||||
.groups
|
||||
.getPushGroupsContainingMember(recipientId)
|
||||
.asSequence()
|
||||
.filter { it.members.contains(Recipient.self().id) }
|
||||
@@ -87,7 +87,7 @@ class ConversationSettingsRepository(
|
||||
|
||||
fun getGroupMembership(recipientId: RecipientId, consumer: (List<RecipientId>) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val groupDatabase = DatabaseFactory.getGroupDatabase(context)
|
||||
val groupDatabase = SignalDatabase.groups
|
||||
val groupRecords = groupDatabase.getPushGroupsContainingMember(recipientId)
|
||||
val groupRecipients = ArrayList<RecipientId>(groupRecords.size)
|
||||
for (groupRecord in groupRecords) {
|
||||
@@ -109,13 +109,13 @@ class ConversationSettingsRepository(
|
||||
|
||||
fun setMuteUntil(recipientId: RecipientId, until: Long) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until)
|
||||
SignalDatabase.recipients.setMuted(recipientId, until)
|
||||
}
|
||||
}
|
||||
|
||||
fun getGroupCapacity(groupId: GroupId, consumer: (GroupCapacityResult) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val groupRecord: GroupDatabase.GroupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get()
|
||||
val groupRecord: GroupDatabase.GroupRecord = SignalDatabase.groups.getGroup(groupId).get()
|
||||
consumer(
|
||||
if (groupRecord.isV2Group) {
|
||||
val decryptedGroup: DecryptedGroup = groupRecord.requireV2GroupProperties().decryptedGroup
|
||||
@@ -138,7 +138,7 @@ class ConversationSettingsRepository(
|
||||
|
||||
fun addMembers(groupId: GroupId, selected: List<RecipientId>, consumer: (GroupAddMembersResult) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val record: GroupDatabase.GroupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get()
|
||||
val record: GroupDatabase.GroupRecord = SignalDatabase.groups.getGroup(groupId).get()
|
||||
|
||||
if (record.isAnnouncementGroup) {
|
||||
val needsResolve = selected
|
||||
@@ -171,7 +171,7 @@ class ConversationSettingsRepository(
|
||||
fun setMuteUntil(groupId: GroupId, until: Long) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipientId = Recipient.externalGroupExact(context, groupId).id
|
||||
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until)
|
||||
SignalDatabase.recipients.setMuted(recipientId, until)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,10 +103,8 @@ sealed class ConversationSettingsViewModel(
|
||||
|
||||
override fun onCleared() {
|
||||
cleared = true
|
||||
store.update { state ->
|
||||
openedMediaCursors.forEach { it.ensureClosed() }
|
||||
state.copy(sharedMedia = null)
|
||||
}
|
||||
openedMediaCursors.forEach { it.ensureClosed() }
|
||||
store.clear()
|
||||
}
|
||||
|
||||
private fun Cursor?.ensureClosed() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.text.TextUtils
|
||||
import android.widget.Toast
|
||||
@@ -15,19 +14,20 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
|
||||
import org.thoughtcrime.securesms.components.settings.configure
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.Hex
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import java.util.Objects
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Shows internal details about a recipient that you can view from the conversation settings.
|
||||
@@ -60,7 +60,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
)
|
||||
|
||||
if (!recipient.isGroup) {
|
||||
val uuid = recipient.uuid.transform(UUID::toString).or("null")
|
||||
val uuid = recipient.aci.transform(ACI::toString).or("null")
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("UUID"),
|
||||
summary = DSLSettingsText.from(uuid),
|
||||
@@ -132,7 +132,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> DatabaseFactory.getRecipientDatabase(requireContext()).setProfileSharing(recipient.id, false) }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> SignalDatabase.recipients.setProfileSharing(recipient.id, false) }
|
||||
.show()
|
||||
}
|
||||
)
|
||||
@@ -145,17 +145,40 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
.setTitle("Are you sure?")
|
||||
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
if (recipient.hasUuid()) {
|
||||
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipient.requireUuid().toString())
|
||||
if (recipient.hasAci()) {
|
||||
SignalDatabase.sessions.deleteAllFor(recipient.requireAci().toString())
|
||||
}
|
||||
if (recipient.hasE164()) {
|
||||
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipient.requireE164())
|
||||
SignalDatabase.sessions.deleteAllFor(recipient.requireE164())
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (recipient.isSelf) {
|
||||
sectionHeaderPref(DSLSettingsText.from("Donations"))
|
||||
|
||||
val subscriber: Subscriber? = SignalStore.donationsValues().getSubscriber()
|
||||
val summary = if (subscriber != null) {
|
||||
"""currency code: ${subscriber.currencyCode}
|
||||
|subscriber id: ${subscriber.subscriberId.serialize()}
|
||||
""".trimMargin()
|
||||
} else {
|
||||
"None"
|
||||
}
|
||||
|
||||
longClickPref(
|
||||
title = DSLSettingsText.from("Subscriber ID"),
|
||||
summary = DSLSettingsText.from(summary),
|
||||
onLongClick = {
|
||||
if (subscriber != null) {
|
||||
copyToClipboard(subscriber.subscriberId.serialize())
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,9 +228,8 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
||||
liveRecipient.observeForever(this)
|
||||
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val context: Context = ApplicationDependencies.getApplication()
|
||||
val threadId: Long? = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipientId)
|
||||
val groupId: GroupId? = DatabaseFactory.getGroupDatabase(context).getGroup(recipientId).transform { it.id }.orNull()
|
||||
val threadId: Long? = SignalDatabase.threads.getThreadIdFor(recipientId)
|
||||
val groupId: GroupId? = SignalDatabase.groups.getGroup(recipientId).transform { it.id }.orNull()
|
||||
store.update { state -> state.copy(threadId = threadId, groupId = groupId) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,13 +49,19 @@ object AvatarPreference {
|
||||
}
|
||||
|
||||
override fun bind(model: Model) {
|
||||
badge.setBadgeFromRecipient(model.recipient)
|
||||
badge.setOnClickListener {
|
||||
val badge = model.recipient.badges.firstOrNull()
|
||||
if (badge != null) {
|
||||
model.onBadgeClick(badge)
|
||||
if (model.recipient.isSelf) {
|
||||
badge.setBadge(null)
|
||||
badge.setOnClickListener(null)
|
||||
} else {
|
||||
badge.setBadgeFromRecipient(model.recipient)
|
||||
badge.setOnClickListener {
|
||||
val badge = model.recipient.badges.firstOrNull()
|
||||
if (badge != null) {
|
||||
model.onBadgeClick(badge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
avatar.setAvatar(model.recipient)
|
||||
avatar.disableQuickContact()
|
||||
avatar.setOnClickListener { model.onAvatarClick(avatar) }
|
||||
|
||||
@@ -41,27 +41,24 @@ object ButtonStripPreference {
|
||||
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
|
||||
private val message: View = itemView.findViewById(R.id.message)
|
||||
private val messageLabel: View = itemView.findViewById(R.id.message_label)
|
||||
private val messageContainer: View = itemView.findViewById(R.id.button_strip_message_container)
|
||||
private val videoCall: View = itemView.findViewById(R.id.start_video)
|
||||
private val videoLabel: View = itemView.findViewById(R.id.start_video_label)
|
||||
private val videoContainer: View = itemView.findViewById(R.id.button_strip_video_container)
|
||||
private val audioCall: ImageView = itemView.findViewById(R.id.start_audio)
|
||||
private val audioLabel: TextView = itemView.findViewById(R.id.start_audio_label)
|
||||
private val audioContainer: View = itemView.findViewById(R.id.button_strip_audio_container)
|
||||
private val mute: ImageView = itemView.findViewById(R.id.mute)
|
||||
private val muteLabel: TextView = itemView.findViewById(R.id.mute_label)
|
||||
private val muteContainer: View = itemView.findViewById(R.id.button_strip_mute_container)
|
||||
private val search: View = itemView.findViewById(R.id.search)
|
||||
private val searchLabel: View = itemView.findViewById(R.id.search_label)
|
||||
private val searchContainer: View = itemView.findViewById(R.id.button_strip_search_container)
|
||||
|
||||
override fun bind(model: Model) {
|
||||
message.visible = model.state.isMessageAvailable
|
||||
messageLabel.visible = model.state.isMessageAvailable
|
||||
videoCall.visible = model.state.isVideoAvailable
|
||||
videoLabel.visible = model.state.isVideoAvailable
|
||||
audioCall.visible = model.state.isAudioAvailable
|
||||
audioLabel.visible = model.state.isAudioAvailable
|
||||
mute.visible = model.state.isMuteAvailable
|
||||
muteLabel.visible = model.state.isMuteAvailable
|
||||
search.visible = model.state.isSearchAvailable
|
||||
searchLabel.visible = model.state.isSearchAvailable
|
||||
messageContainer.visible = model.state.isMessageAvailable
|
||||
videoContainer.visible = model.state.isVideoAvailable
|
||||
audioContainer.visible = model.state.isAudioAvailable
|
||||
muteContainer.visible = model.state.isMuteAvailable
|
||||
searchContainer.visible = model.state.isSearchAvailable
|
||||
|
||||
if (model.state.isAudioSecure) {
|
||||
audioLabel.setText(R.string.ConversationSettingsFragment__audio)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user