mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-12 21:13:18 +01:00
Compare commits
197 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0323cb5d98 | ||
|
|
f4369f90e0 | ||
|
|
8b19cbb603 | ||
|
|
aa3a797e19 | ||
|
|
827ceafffb | ||
|
|
cf1afb739f | ||
|
|
b9fe377afd | ||
|
|
a381697949 | ||
|
|
2d87078495 | ||
|
|
1b9695cb98 | ||
|
|
5324290fab | ||
|
|
b8e4ffb5ae | ||
|
|
67a693107e | ||
|
|
e08b86cda6 | ||
|
|
92bab9fb20 | ||
|
|
e7502f08ce | ||
|
|
3a530022fc | ||
|
|
2c8144b32f | ||
|
|
87535a917a | ||
|
|
76448f5426 | ||
|
|
019df97a22 | ||
|
|
51897bb74f | ||
|
|
5f3b4056e9 | ||
|
|
73a3c21716 | ||
|
|
a37209d8ba | ||
|
|
415021eedf | ||
|
|
ea6d512cc8 | ||
|
|
fba6673907 | ||
|
|
faba4682ed | ||
|
|
71b92f03bc | ||
|
|
d4a1cb0bfb | ||
|
|
e16ca2b2d2 | ||
|
|
77e678e05c | ||
|
|
efe0e3b816 | ||
|
|
6c497e131a | ||
|
|
ccb8c1b1b9 | ||
|
|
4aa965144d | ||
|
|
786bcc3da7 | ||
|
|
4447b29e6c | ||
|
|
3ebbb94a1a | ||
|
|
64a7cdafa8 | ||
|
|
c3350c0bb0 | ||
|
|
e2be1e0c79 | ||
|
|
228a993237 | ||
|
|
04923487c4 | ||
|
|
9777aa411c | ||
|
|
d0c1e93b3c | ||
|
|
9b517a14cb | ||
|
|
369085e162 | ||
|
|
93815a0504 | ||
|
|
b88097a6ae | ||
|
|
120cc9c521 | ||
|
|
58304a0fb6 | ||
|
|
6e867d678c | ||
|
|
8b2f58e0e7 | ||
|
|
6976ac7d44 | ||
|
|
8dc2077ad0 | ||
|
|
52fa86046b | ||
|
|
3352ebaa06 | ||
|
|
cbfdc4b57a | ||
|
|
c5753b96ff | ||
|
|
f39ad24cc1 | ||
|
|
6b6877bae7 | ||
|
|
930254da7b | ||
|
|
3df2fa53e8 | ||
|
|
c901639ce8 | ||
|
|
9e1cec7a60 | ||
|
|
9269c66d1e | ||
|
|
fd999be41a | ||
|
|
146a5f5701 | ||
|
|
d49ef1dd7d | ||
|
|
49c5fead39 | ||
|
|
9c705f3a45 | ||
|
|
bea204ab82 | ||
|
|
9350438866 | ||
|
|
4d827adc8b | ||
|
|
9f839b75fb | ||
|
|
c0482e8247 | ||
|
|
17f27f45fc | ||
|
|
2401e33222 | ||
|
|
4345179a1d | ||
|
|
5aa6fc78ee | ||
|
|
e0a86ead58 | ||
|
|
169d0fa964 | ||
|
|
c5397bc7d2 | ||
|
|
43f6e0ad8e | ||
|
|
736811393f | ||
|
|
957ddc82b5 | ||
|
|
16d6e98355 | ||
|
|
2a90809ba3 | ||
|
|
0713a88ddb | ||
|
|
c78b47fbe3 | ||
|
|
5807cbc9e9 | ||
|
|
6d90330e86 | ||
|
|
862bab55af | ||
|
|
7235a3730c | ||
|
|
c24993960d | ||
|
|
7f429dc769 | ||
|
|
a575626abb | ||
|
|
0b71b1837c | ||
|
|
f0df1b99e5 | ||
|
|
23b7ea90a1 | ||
|
|
53a6b0c719 | ||
|
|
bf3135b2d0 | ||
|
|
897461b594 | ||
|
|
63800306a0 | ||
|
|
b649b8c943 | ||
|
|
2c0aa40c61 | ||
|
|
2eb4f650d8 | ||
|
|
7af811eb3f | ||
|
|
d7f43c436e | ||
|
|
2792b9e676 | ||
|
|
bdf2ef5a05 | ||
|
|
23b5a3dcb0 | ||
|
|
909ea6b925 | ||
|
|
a5922c31b1 | ||
|
|
d8758bcc4e | ||
|
|
f88181cc82 | ||
|
|
c3f1036686 | ||
|
|
96292cd4a1 | ||
|
|
81f6035027 | ||
|
|
52005cf62c | ||
|
|
f5effa5be9 | ||
|
|
cae7906f04 | ||
|
|
7ea8cc6b0a | ||
|
|
8669a3d6e0 | ||
|
|
cb3bc91865 | ||
|
|
1a0c4b8135 | ||
|
|
6a456a288d | ||
|
|
901a81fb74 | ||
|
|
b1b99855b2 | ||
|
|
c6f0b4cf83 | ||
|
|
1a5dede780 | ||
|
|
2c8b1c6acb | ||
|
|
d7da56b82f | ||
|
|
d9cfdd1b32 | ||
|
|
b3b3a4bebf | ||
|
|
9021883baa | ||
|
|
c19017f037 | ||
|
|
bff40ff60b | ||
|
|
299445d5f9 | ||
|
|
b2e3d7ba20 | ||
|
|
60df7502ee | ||
|
|
28ea4dbc16 | ||
|
|
c4d9942f0e | ||
|
|
3e50d2318f | ||
|
|
040881e5a6 | ||
|
|
681234ace3 | ||
|
|
98e9694b35 | ||
|
|
95c46b9d82 | ||
|
|
76dfa5d7fe | ||
|
|
b39d562d56 | ||
|
|
148cf63a92 | ||
|
|
c155b4e025 | ||
|
|
90ae9e1636 | ||
|
|
79ee14826d | ||
|
|
179bb6e1da | ||
|
|
c393d65ce6 | ||
|
|
eeb8164c18 | ||
|
|
ea772cbf55 | ||
|
|
dd67398a70 | ||
|
|
d44bed0379 | ||
|
|
236c79bfbb | ||
|
|
7dfee7e315 | ||
|
|
6600849cc4 | ||
|
|
c4255157ac | ||
|
|
3bb6a0a560 | ||
|
|
a05d5ff5e6 | ||
|
|
da6ad2b629 | ||
|
|
09a05c9f4c | ||
|
|
57319d3189 | ||
|
|
40ba967192 | ||
|
|
3c7534f7fa | ||
|
|
79ec47f901 | ||
|
|
6108b5ab77 | ||
|
|
49417bdf9d | ||
|
|
d2fcb191b6 | ||
|
|
dca876e40d | ||
|
|
5e35c209c2 | ||
|
|
22382bc8a3 | ||
|
|
bf9e75d983 | ||
|
|
f96e29c9c9 | ||
|
|
0bf2f9aca7 | ||
|
|
2ba427e3dd | ||
|
|
624ae32a0e | ||
|
|
1339c44892 | ||
|
|
8499402831 | ||
|
|
4df6b87c13 | ||
|
|
7d16e857d4 | ||
|
|
a6c215801b | ||
|
|
b3a9b92717 | ||
|
|
071496e0c1 | ||
|
|
af36b9adbd | ||
|
|
630f998ea4 | ||
|
|
9058f7ed55 | ||
|
|
0dac6344ab | ||
|
|
8adb16912f |
@@ -12,6 +12,7 @@ plugins {
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.ktlint)
|
||||
alias(libs.plugins.compose.compiler)
|
||||
alias(libs.plugins.kotlinx.serialization)
|
||||
id("androidx.navigation.safeargs")
|
||||
id("kotlin-parcelize")
|
||||
id("com.squareup.wire")
|
||||
@@ -21,8 +22,8 @@ plugins {
|
||||
|
||||
apply(from = "static-ips.gradle.kts")
|
||||
|
||||
val canonicalVersionCode = 1581
|
||||
val canonicalVersionName = "7.56.1"
|
||||
val canonicalVersionCode = 1596
|
||||
val canonicalVersionName = "7.59.1"
|
||||
val currentHotfixVersion = 0
|
||||
val maxHotfixVersions = 100
|
||||
|
||||
@@ -217,7 +218,7 @@ android {
|
||||
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY_LEGACY", "\"9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY", "\"093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
|
||||
buildConfigField("String[]", "UNIDENTIFIED_SENDER_TRUST_ROOTS", "new String[]{ \"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\", \"BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6\"}")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"")
|
||||
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O\"")
|
||||
@@ -237,8 +238,7 @@ android {
|
||||
buildConfigField("String", "STRIPE_BASE_URL", "\"https://api.stripe.com/v1\"")
|
||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
|
||||
buildConfigField("boolean", "TRACING_ENABLED", "false")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||
buildConfigField("boolean", "LINK_DEVICE_UX_ENABLED", "true")
|
||||
buildConfigField("boolean", "LINK_DEVICE_UX_ENABLED", "false")
|
||||
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
@@ -378,7 +378,6 @@ android {
|
||||
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
|
||||
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "\"${apkUpdateManifestUrl}\"")
|
||||
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\"")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||
}
|
||||
|
||||
create("prod") {
|
||||
@@ -405,7 +404,7 @@ android {
|
||||
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY_LEGACY", "\"38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY", "\"2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
|
||||
buildConfigField("String[]", "UNIDENTIFIED_SENDER_TRUST_ROOTS", "new String[]{\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\", \"BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm\"}")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"")
|
||||
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8\"")
|
||||
@@ -417,7 +416,6 @@ android {
|
||||
|
||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
|
||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||
}
|
||||
|
||||
create("backup") {
|
||||
@@ -429,7 +427,6 @@ android {
|
||||
|
||||
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
|
||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Backup\"")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +604,7 @@ dependencies {
|
||||
implementation(libs.rxdogtag)
|
||||
implementation(libs.androidx.credentials)
|
||||
implementation(libs.androidx.credentials.compat)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(project(":billing"))
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -171,6 +171,11 @@ class ArchiveImportExportTests {
|
||||
runTests { it.startsWith("chat_item_standard_message_standard_attachments_") }
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun chatItemStandardMessageGroupTextOnly() {
|
||||
runTests { it.startsWith("chat_item_standard_message_group_text_only_") }
|
||||
}
|
||||
|
||||
// @Test
|
||||
fun chatItemStandardMessageTextOnly() {
|
||||
runTests { it.startsWith("chat_item_standard_message_text_only_") }
|
||||
|
||||
@@ -21,7 +21,6 @@ import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isNull
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
@@ -43,7 +42,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
|
||||
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@@ -67,9 +65,6 @@ class MessageBackupsCheckoutActivityTest {
|
||||
every { AppDependencies.billingApi.getBillingPurchaseResults() } returns purchaseResults
|
||||
coEvery { AppDependencies.billingApi.queryProduct() } returns BillingProduct(price = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")))
|
||||
coEvery { AppDependencies.billingApi.launchBillingFlow(any()) } returns Unit
|
||||
|
||||
mockkStatic(RemoteConfig::class)
|
||||
every { RemoteConfig.messageBackups } returns true
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -36,6 +36,8 @@ import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.polls.PollOption
|
||||
import org.thoughtcrime.securesms.polls.PollRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
@@ -348,5 +350,11 @@ class V2ConversationItemShapeTest {
|
||||
override fun onShowUnverifiedProfileSheet(forGroup: Boolean) = Unit
|
||||
|
||||
override fun onUpdateSignalClicked() = Unit
|
||||
|
||||
override fun onViewResultsClicked(pollId: Long) = Unit
|
||||
|
||||
override fun onViewPollClicked(messageId: Long) = Unit
|
||||
|
||||
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +259,7 @@ class AttachmentTableTest {
|
||||
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
||||
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
|
||||
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(AttachmentId(1L), AttachmentTableTestUtil.createUploadResult(AttachmentId(1L)))
|
||||
|
||||
// WHEN
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()
|
||||
@@ -277,6 +278,7 @@ class AttachmentTableTest {
|
||||
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
||||
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
|
||||
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(AttachmentId(1L), AttachmentTableTestUtil.createUploadResult(AttachmentId(1L)))
|
||||
|
||||
// WHEN
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()
|
||||
@@ -296,6 +298,7 @@ class AttachmentTableTest {
|
||||
SignalDatabase.messages.markExpireStarted(messageId)
|
||||
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
|
||||
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(AttachmentId(1L), AttachmentTableTestUtil.createUploadResult(AttachmentId(1L)))
|
||||
|
||||
// WHEN
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()
|
||||
@@ -314,6 +317,7 @@ class AttachmentTableTest {
|
||||
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
|
||||
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
|
||||
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(AttachmentId(1L), AttachmentTableTestUtil.createUploadResult(AttachmentId(1L)))
|
||||
|
||||
// WHEN
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()
|
||||
|
||||
@@ -47,6 +47,8 @@ class AttachmentTableTest_deduping {
|
||||
val DATA_A_HASH = byteArrayOf(1, 1, 1)
|
||||
|
||||
val DATA_B = byteArrayOf(7, 8, 9)
|
||||
|
||||
val DATA_C_JPEG = Base64.decode("/9j/4AAQSkZJRgABAQEBLAEsAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wgARCAAKAAoDAREAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAWAQEBAQAAAAAAAAAAAAAAAAAABgf/2gAMAwEAAhADEAAAAY/ZpAAf/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQABBQIf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQAGPwIf/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQABPyEf/9oADAMBAAIAAwAAABBtv//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8QH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8QH//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEAAT8QH//Z")
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -483,38 +485,27 @@ class AttachmentTableTest_deduping {
|
||||
fun quotes() {
|
||||
// Basic quote deduping
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
val targetAttachment1 = insertWithData(DATA_C_JPEG)
|
||||
val quoteAttachment1 = insertQuote(targetAttachment1)
|
||||
val quoteAttachment2 = insertQuote(targetAttachment1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataFilesAreTheSame(quoteAttachment1, quoteAttachment2)
|
||||
assertDataHashStartMatches(quoteAttachment1, quoteAttachment2)
|
||||
}
|
||||
|
||||
// Making sure remote fields carry
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
upload(id1)
|
||||
val targetAttachment1 = insertWithData(DATA_C_JPEG)
|
||||
val quoteAttachment1 = insertQuote(targetAttachment1)
|
||||
upload(quoteAttachment1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
val quoteAttachment2 = insertQuote(targetAttachment1)
|
||||
|
||||
// Making sure things work for quotes of videos, which have trickier transform properties
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, transformProperties = TransformProperties.forVideoTrim(1, 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1)
|
||||
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
assertDataFilesAreTheSame(quoteAttachment1, quoteAttachment2)
|
||||
assertDataHashStartMatches(quoteAttachment1, quoteAttachment2)
|
||||
assertDataHashEndMatches(quoteAttachment1, quoteAttachment2)
|
||||
assertRemoteFieldsMatch(quoteAttachment1, quoteAttachment2)
|
||||
assertArchiveFieldsMatch(quoteAttachment1, quoteAttachment2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.junit.runner.RunWith
|
||||
import org.signal.core.util.count
|
||||
import org.signal.core.util.readToSingleInt
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject
|
||||
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable.ArchiveMediaItem
|
||||
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable.MediaEntry
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -21,7 +21,7 @@ class BackupMediaSnapshotTableTest {
|
||||
|
||||
@Test
|
||||
fun givenAnEmptyTable_whenIWriteToTable_thenIExpectEmptyTable() {
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = 100))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = 100))
|
||||
|
||||
val count = getCountForLatestSnapshot(includeThumbnails = true)
|
||||
|
||||
@@ -30,7 +30,7 @@ class BackupMediaSnapshotTableTest {
|
||||
|
||||
@Test
|
||||
fun givenAnEmptyTable_whenIWriteToTableAndCommit_thenIExpectFilledTable() {
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = 100))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = 100))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val count = getCountForLatestSnapshot(includeThumbnails = false)
|
||||
@@ -43,7 +43,8 @@ class BackupMediaSnapshotTableTest {
|
||||
val inputCount = 100
|
||||
val countWithThumbnails = inputCount * 2
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = inputCount))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = inputCount, thumbnail = true))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val count = getCountForLatestSnapshot(includeThumbnails = true)
|
||||
@@ -51,40 +52,16 @@ class BackupMediaSnapshotTableTest {
|
||||
assertThat(count).isEqualTo(countWithThumbnails)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnEmptyTable_whenIWriteToTableAndCommitQuotes_thenIExpectFilledTableWithNoThumbnails() {
|
||||
val inputCount = 100
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount, quote = true))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val count = getCountForLatestSnapshot(includeThumbnails = true)
|
||||
|
||||
assertThat(count).isEqualTo(inputCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnEmptyTable_whenIWriteToTableAndCommitNonMedia_thenIExpectFilledTableWithNoThumbnails() {
|
||||
val inputCount = 100
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount, contentType = "text/plain"))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val count = getCountForLatestSnapshot(includeThumbnails = true)
|
||||
|
||||
assertThat(count).isEqualTo(inputCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAFilledTable_whenIReinsertObjects_thenIExpectUncommittedOverrides() {
|
||||
val initialCount = 100
|
||||
val additionalCount = 25
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = initialCount))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = initialCount))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
// This relies on how the sequence of mediaIds is generated in tests -- the ones we generate here will have the mediaIds as the ones we generated above
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = additionalCount))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = additionalCount))
|
||||
|
||||
val pendingCount = getCountForPending(includeThumbnails = false)
|
||||
val latestVersionCount = getCountForLatestSnapshot(includeThumbnails = false)
|
||||
@@ -98,11 +75,11 @@ class BackupMediaSnapshotTableTest {
|
||||
val initialCount = 100
|
||||
val additionalCount = 25
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = initialCount))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = initialCount))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
// This relies on how the sequence of mediaIds is generated in tests -- the ones we generate here will have the mediaIds as the ones we generated above
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = additionalCount))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = additionalCount))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val pendingCount = getCountForPending(includeThumbnails = false)
|
||||
@@ -119,10 +96,10 @@ class BackupMediaSnapshotTableTest {
|
||||
val initialCount = 100
|
||||
val additionalCount = 25
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = initialCount))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = initialCount))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = additionalCount))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = additionalCount))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val page = SignalDatabase.backupMediaSnapshots.getPageOfOldMediaObjects(pageSize = 1_000)
|
||||
@@ -145,7 +122,7 @@ class BackupMediaSnapshotTableTest {
|
||||
createArchiveMediaObject(seed = 2, cdn = 2)
|
||||
)
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(localData.asSequence())
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(localData)
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val mismatches = SignalDatabase.backupMediaSnapshots.getMediaObjectsWithNonMatchingCdn(remoteData)
|
||||
@@ -164,7 +141,7 @@ class BackupMediaSnapshotTableTest {
|
||||
createArchiveMediaObject(seed = 2, cdn = 99)
|
||||
)
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(localData.asSequence())
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(localData)
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val mismatches = SignalDatabase.backupMediaSnapshots.getMediaObjectsWithNonMatchingCdn(remoteData)
|
||||
@@ -186,7 +163,7 @@ class BackupMediaSnapshotTableTest {
|
||||
createArchiveMediaObject(seed = 2, cdn = 2)
|
||||
)
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(localData.asSequence())
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(localData)
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val notFound = SignalDatabase.backupMediaSnapshots.getMediaObjectsThatCantBeFound(remoteData)
|
||||
@@ -205,7 +182,7 @@ class BackupMediaSnapshotTableTest {
|
||||
createArchiveMediaObject(seed = 3, cdn = 2)
|
||||
)
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(localData.asSequence())
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(localData)
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val notFound = SignalDatabase.backupMediaSnapshots.getMediaObjectsThatCantBeFound(remoteData)
|
||||
@@ -222,7 +199,7 @@ class BackupMediaSnapshotTableTest {
|
||||
|
||||
@Test
|
||||
fun getCurrentSnapshotVersion_singleCommit() {
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = 100))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = 100))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val version = SignalDatabase.backupMediaSnapshots.getCurrentSnapshotVersion()
|
||||
@@ -234,14 +211,12 @@ class BackupMediaSnapshotTableTest {
|
||||
fun getMediaObjectsLastSeenOnCdnBeforeSnapshotVersion_noneMarkedSeen() {
|
||||
val initialCount = 100
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = initialCount))
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = initialCount))
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val notSeenCount = SignalDatabase.backupMediaSnapshots.getMediaObjectsLastSeenOnCdnBeforeSnapshotVersion(1).count
|
||||
|
||||
val expectedOldCountIncludingThumbnails = initialCount * 2
|
||||
|
||||
assertThat(notSeenCount).isEqualTo(expectedOldCountIncludingThumbnails)
|
||||
assertThat(notSeenCount).isEqualTo(initialCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -249,22 +224,25 @@ class BackupMediaSnapshotTableTest {
|
||||
val initialCount = 100
|
||||
val markSeenCount = 25
|
||||
|
||||
val itemsToCommit = generateArchiveMediaItemSequence(count = initialCount)
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(itemsToCommit)
|
||||
val fullSizeItems = generateArchiveMediaItemSequence(count = initialCount, thumbnail = false)
|
||||
val thumbnailItems = generateArchiveMediaItemSequence(count = initialCount, thumbnail = true)
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(fullSizeItems)
|
||||
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(thumbnailItems)
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.commitPendingRows()
|
||||
|
||||
val normalIdsToMarkSeen = itemsToCommit.take(markSeenCount).map { it.mediaId }.toList()
|
||||
val thumbnailIdsToMarkSeen = itemsToCommit.take(markSeenCount).map { it.thumbnailMediaId }.toList()
|
||||
val allItemsToMarkSeen = normalIdsToMarkSeen + thumbnailIdsToMarkSeen
|
||||
val fullSizeIdsToMarkSeen = fullSizeItems.take(markSeenCount).map { it.mediaId }.toList()
|
||||
val thumbnailIdsToMarkSeen = thumbnailItems.take(markSeenCount).map { it.mediaId }.toList()
|
||||
|
||||
SignalDatabase.backupMediaSnapshots.markSeenOnRemote(allItemsToMarkSeen, 1)
|
||||
SignalDatabase.backupMediaSnapshots.markSeenOnRemote(fullSizeIdsToMarkSeen, 1)
|
||||
SignalDatabase.backupMediaSnapshots.markSeenOnRemote(thumbnailIdsToMarkSeen, 1)
|
||||
|
||||
val notSeenCount = SignalDatabase.backupMediaSnapshots.getMediaObjectsLastSeenOnCdnBeforeSnapshotVersion(1).count
|
||||
|
||||
val expectedOldCount = initialCount - markSeenCount
|
||||
val expectedOldCountIncludingThumbnails = expectedOldCount * 2
|
||||
val expectedOldCount = (initialCount * 2) - (markSeenCount * 2)
|
||||
|
||||
assertThat(notSeenCount).isEqualTo(expectedOldCountIncludingThumbnails)
|
||||
assertThat(notSeenCount).isEqualTo(expectedOldCount)
|
||||
}
|
||||
|
||||
private fun getTotalItemCount(includeThumbnails: Boolean): Int {
|
||||
@@ -314,28 +292,30 @@ class BackupMediaSnapshotTableTest {
|
||||
.readToSingleInt(0)
|
||||
}
|
||||
|
||||
private fun generateArchiveMediaItemSequence(count: Int, quote: Boolean = false, contentType: String = "image/jpeg"): Sequence<ArchiveMediaItem> {
|
||||
private fun generateArchiveMediaItemSequence(count: Int, thumbnail: Boolean = false): Collection<MediaEntry> {
|
||||
return (1..count)
|
||||
.asSequence()
|
||||
.map { createArchiveMediaItem(it, quote = quote, contentType = contentType) }
|
||||
.map { createArchiveMediaItem(it, thumbnail = thumbnail) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun createArchiveMediaItem(seed: Int, cdn: Int = 0, quote: Boolean = false, contentType: String = "image/jpeg"): ArchiveMediaItem {
|
||||
return ArchiveMediaItem(
|
||||
mediaId = "media_id_$seed",
|
||||
thumbnailMediaId = "thumbnail_media_id_$seed",
|
||||
private fun createArchiveMediaItem(seed: Int, thumbnail: Boolean = false, cdn: Int = 0): MediaEntry {
|
||||
return MediaEntry(
|
||||
mediaId = mediaId(seed, thumbnail),
|
||||
cdn = cdn,
|
||||
plaintextHash = Util.toByteArray(seed),
|
||||
remoteKey = Util.toByteArray(seed),
|
||||
quote = quote,
|
||||
contentType = contentType
|
||||
isThumbnail = thumbnail
|
||||
)
|
||||
}
|
||||
|
||||
private fun createArchiveMediaObject(seed: Int, cdn: Int = 0): ArchivedMediaObject {
|
||||
private fun createArchiveMediaObject(seed: Int, thumbnail: Boolean = false, cdn: Int = 0): ArchivedMediaObject {
|
||||
return ArchivedMediaObject(
|
||||
mediaId = "media_id_$seed",
|
||||
mediaId = mediaId(seed, thumbnail),
|
||||
cdn = cdn
|
||||
)
|
||||
}
|
||||
|
||||
fun mediaId(seed: Int, thumbnail: Boolean): String {
|
||||
return "media_id_${seed}_$thumbnail"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,15 +9,10 @@ import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireLongOrNull
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyPair
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyType
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.signal.libsignal.protocol.ReusedBaseKeyException
|
||||
import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil.generateECPublicKey
|
||||
import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil.getStaleTime
|
||||
import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil.insertTestRecord
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
@@ -142,42 +137,43 @@ class KyberPreKeyTableTest {
|
||||
assertNotNull(getStaleTime(aci, 3))
|
||||
}
|
||||
|
||||
private fun insertTestRecord(account: ServiceId, id: Int, staleTime: Long = 0, lastResort: Boolean = false) {
|
||||
val kemKeyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024)
|
||||
SignalDatabase.kyberPreKeys.insert(
|
||||
serviceId = account,
|
||||
keyId = id,
|
||||
record = KyberPreKeyRecord(
|
||||
id,
|
||||
System.currentTimeMillis(),
|
||||
kemKeyPair,
|
||||
ECKeyPair.generate().privateKey.calculateSignature(kemKeyPair.publicKey.serialize())
|
||||
),
|
||||
lastResort = lastResort
|
||||
@Test(expected = ReusedBaseKeyException::class)
|
||||
fun handleMarkKyberPreKeyUsed_doesNotAllowDuplicateLastResortKeyEntries() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10, lastResort = true)
|
||||
val publicKey = generateECPublicKey()
|
||||
|
||||
SignalDatabase.kyberPreKeys.handleMarkKyberPreKeyUsed(
|
||||
serviceId = aci,
|
||||
kyberPreKeyId = 1,
|
||||
signedPreKeyId = 1,
|
||||
baseKey = publicKey
|
||||
)
|
||||
|
||||
val count = SignalDatabase.rawDatabase
|
||||
.update(KyberPreKeyTable.TABLE_NAME)
|
||||
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
|
||||
assertEquals(1, count)
|
||||
SignalDatabase.kyberPreKeys.handleMarkKyberPreKeyUsed(
|
||||
serviceId = aci,
|
||||
kyberPreKeyId = 1,
|
||||
signedPreKeyId = 1,
|
||||
baseKey = publicKey
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStaleTime(account: ServiceId, id: Int): Long? {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select(KyberPreKeyTable.STALE_TIMESTAMP)
|
||||
.from(KyberPreKeyTable.TABLE_NAME)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
|
||||
}
|
||||
@Test
|
||||
fun handleMarkKyberPreKeyUsed_allowDuplicateNonLastResortKeyEntries() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10, lastResort = false)
|
||||
val publicKey = generateECPublicKey()
|
||||
|
||||
private fun ServiceId.toAccountId(): String {
|
||||
return when (this) {
|
||||
is ACI -> this.toString()
|
||||
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
|
||||
}
|
||||
SignalDatabase.kyberPreKeys.handleMarkKyberPreKeyUsed(
|
||||
serviceId = aci,
|
||||
kyberPreKeyId = 1,
|
||||
signedPreKeyId = 1,
|
||||
baseKey = publicKey
|
||||
)
|
||||
|
||||
SignalDatabase.kyberPreKeys.handleMarkKyberPreKeyUsed(
|
||||
serviceId = aci,
|
||||
kyberPreKeyId = 1,
|
||||
signedPreKeyId = 1,
|
||||
baseKey = publicKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.deleteAll
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.polls.PollOption
|
||||
import org.thoughtcrime.securesms.polls.PollRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PollTablesTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var poll1: PollRecord
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
poll1 = PollRecord(
|
||||
id = 1,
|
||||
question = "how do you feel about unit testing?",
|
||||
pollOptions = listOf(
|
||||
PollOption(1, "yay", listOf(1)),
|
||||
PollOption(2, "ok", emptyList()),
|
||||
PollOption(3, "nay", emptyList())
|
||||
),
|
||||
allowMultipleVotes = false,
|
||||
hasEnded = false,
|
||||
authorId = 1,
|
||||
messageId = 1
|
||||
)
|
||||
|
||||
SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollTable.TABLE_NAME)
|
||||
SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollOptionTable.TABLE_NAME)
|
||||
SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollVoteTable.TABLE_NAME)
|
||||
|
||||
val message = IncomingMessage(type = MessageType.NORMAL, from = harness.others[0], sentTimeMillis = 100, serverTimeMillis = 100, receivedTimeMillis = 100)
|
||||
SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(harness.others[0], isGroup = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPollWithVoting_whenIGetPoll_thenIExpectThatPoll() {
|
||||
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
|
||||
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 1, messageId = MessageId(1))
|
||||
|
||||
assertEquals(poll1, SignalDatabase.polls.getPoll(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPoll_whenIGetItsOptionIds_thenIExpectAllOptionsIds() {
|
||||
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
|
||||
assertEquals(poll1.pollOptions.map { it.id }, SignalDatabase.polls.getPollOptionIds(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPollAndVoter_whenIGetItsVoteCount_thenIExpectTheCorrectVoterCount() {
|
||||
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
|
||||
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 1, messageId = MessageId(1))
|
||||
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(2), voterId = 2, voteCount = 2, messageId = MessageId(1))
|
||||
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(3), voterId = 3, voteCount = 3, messageId = MessageId(1))
|
||||
|
||||
assertEquals(1, SignalDatabase.polls.getCurrentPollVoteCount(1, 1))
|
||||
assertEquals(2, SignalDatabase.polls.getCurrentPollVoteCount(1, 2))
|
||||
assertEquals(3, SignalDatabase.polls.getCurrentPollVoteCount(1, 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleRoundsOfVoting_whenIGetItsCount_thenIExpectTheMostRecentResults() {
|
||||
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
|
||||
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(2), voterId = 1, voteCount = 1, messageId = MessageId(1))
|
||||
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(3), voterId = 1, voteCount = 2, messageId = MessageId(1))
|
||||
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 3, messageId = MessageId(1))
|
||||
|
||||
assertEquals(poll1, SignalDatabase.polls.getPoll(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPoll_whenITerminateIt_thenIExpectItToEnd() {
|
||||
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
|
||||
SignalDatabase.polls.endPoll(1, System.currentTimeMillis())
|
||||
|
||||
assertEquals(true, SignalDatabase.polls.getPoll(1)!!.hasEnded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPoll_whenIIVote_thenIExpectThatVote() {
|
||||
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
|
||||
val poll = SignalDatabase.polls.getPoll(1)!!
|
||||
val pollOption = poll.pollOptions.first()
|
||||
|
||||
val voteCount = SignalDatabase.polls.insertVote(poll, pollOption)
|
||||
|
||||
assertEquals(1, voteCount)
|
||||
assertEquals(listOf(0), SignalDatabase.polls.getVotes(poll.id, false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPoll_whenIRemoveVote_thenVoteIsCleared() {
|
||||
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
|
||||
val poll = SignalDatabase.polls.getPoll(1)!!
|
||||
val pollOption = poll.pollOptions.first()
|
||||
|
||||
val voteCount = SignalDatabase.polls.removeVote(poll, pollOption)
|
||||
SignalDatabase.polls.markPendingAsRemoved(poll.id, Recipient.self().id.toLong(), voteCount, 1)
|
||||
|
||||
assertEquals(1, voteCount)
|
||||
val status = SignalDatabase.polls.getPollVoteStateForGivenVote(poll.id, voteCount)
|
||||
assertEquals(PollTables.VoteState.REMOVED, status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAVote_whenISetPollOptionId_thenOptionIdIsUpdated() {
|
||||
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
|
||||
val poll = SignalDatabase.polls.getPoll(1)!!
|
||||
val option = poll.pollOptions.first()
|
||||
|
||||
SignalDatabase.polls.insertVotes(poll.id, listOf(option.id), Recipient.self().id.toLong(), 5, MessageId(1))
|
||||
SignalDatabase.polls.setPollVoteStateForGivenVote(poll.id, Recipient.self().id.toLong(), 5, 1, true)
|
||||
val status = SignalDatabase.polls.getPollVoteStateForGivenVote(poll.id, 5)
|
||||
|
||||
assertEquals(PollTables.VoteState.ADDED, status)
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ class BackupDeleteJobTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkObject(RemoteConfig)
|
||||
every { RemoteConfig.messageBackups } returns true
|
||||
every { RemoteConfig.internalUser } returns true
|
||||
every { RemoteConfig.defaultMaxBackoff } returns 1000L
|
||||
|
||||
@@ -53,17 +52,6 @@ class BackupDeleteJobTest {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenBackupsNotEnabled_whenIRun_thenIExpectFailure() {
|
||||
every { RemoteConfig.messageBackups } returns false
|
||||
|
||||
val job = BackupDeleteJob()
|
||||
|
||||
val result = job.run()
|
||||
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenUserNotRegistered_whenIRun_thenIExpectFailure() {
|
||||
mockkObject(SignalStore) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.runner.RunWith
|
||||
import org.signal.core.util.billing.BillingProduct
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.billing.BillingPurchaseState
|
||||
import org.signal.core.util.billing.BillingResponseCode
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
@@ -64,10 +65,9 @@ class BackupSubscriptionCheckJobTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkObject(RemoteConfig)
|
||||
every { RemoteConfig.messageBackups } returns true
|
||||
every { RemoteConfig.internalUser } returns true
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
|
||||
coEvery { AppDependencies.billingApi.queryPurchases() } returns mockk()
|
||||
coEvery { AppDependencies.billingApi.queryProduct() } returns null
|
||||
|
||||
@@ -141,19 +141,9 @@ class BackupSubscriptionCheckJobTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenRemoteBackupsNotAvailable_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
||||
every { RemoteConfig.messageBackups } returns false
|
||||
|
||||
val job = BackupSubscriptionCheckJob.create()
|
||||
val result = job.run()
|
||||
|
||||
assertEarlyExit(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenBillingApiNotAvailable_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns false
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.BILLING_UNAVAILABLE
|
||||
|
||||
val job = BackupSubscriptionCheckJob.create()
|
||||
val result = job.run()
|
||||
@@ -183,16 +173,6 @@ class BackupSubscriptionCheckJobTest {
|
||||
assertEarlyExit(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenInternalOverrideIsSet_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
||||
SignalStore.backup.backupTierInternalOverride = MessageBackupTier.PAID
|
||||
|
||||
val job = BackupSubscriptionCheckJob.create()
|
||||
val result = job.run()
|
||||
|
||||
assertEarlyExit(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPendingPayment_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
||||
mockProduct()
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageType
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DataMessageProcessorTest_polls {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
private lateinit var alice: Recipient
|
||||
private lateinit var bob: Recipient
|
||||
private lateinit var charlie: Recipient
|
||||
private lateinit var groupId: GroupId.V2
|
||||
private lateinit var groupRecipientId: RecipientId
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
alice = Recipient.resolved(harness.others[0])
|
||||
bob = Recipient.resolved(harness.others[1])
|
||||
charlie = Recipient.resolved(harness.others[2])
|
||||
|
||||
val groupInfo = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), alice.asMember(), bob.asMember())
|
||||
groupId = groupInfo.groupId
|
||||
groupRecipientId = groupInfo.recipientId
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollCreate_whenIHaveAValidPollProto_createPoll() {
|
||||
val insertResult = handlePollCreate(
|
||||
pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false),
|
||||
senderRecipient = alice,
|
||||
threadRecipient = Recipient.resolved(groupRecipientId),
|
||||
groupId = groupId
|
||||
)
|
||||
|
||||
assert(insertResult != null)
|
||||
val poll = SignalDatabase.polls.getPoll(insertResult!!.messageId)
|
||||
assert(poll != null)
|
||||
assertThat(poll!!.question).isEqualTo("question?")
|
||||
assertThat(poll.pollOptions.size).isEqualTo(3)
|
||||
assertThat(poll.allowMultipleVotes).isEqualTo(false)
|
||||
assertThat(poll.hasEnded).isEqualTo(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollCreate_whenSenderIsNotInGroup_dropMessage() {
|
||||
val insertResult = handlePollCreate(
|
||||
pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false),
|
||||
senderRecipient = charlie,
|
||||
threadRecipient = Recipient.resolved(groupRecipientId),
|
||||
groupId = groupId
|
||||
)
|
||||
|
||||
assert(insertResult == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollCreate_whenTargetRecipientIsNotAGroup_dropMessage() {
|
||||
val insertResult = handlePollCreate(
|
||||
pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false),
|
||||
senderRecipient = alice,
|
||||
threadRecipient = bob,
|
||||
groupId = null
|
||||
)
|
||||
|
||||
assert(insertResult == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollTerminate_whenIHaveValidProto_endPoll() {
|
||||
val pollMessageId = insertPoll()
|
||||
|
||||
val insertResult = DataMessageProcessor.handlePollTerminate(
|
||||
context = ApplicationProvider.getApplicationContext(),
|
||||
envelope = MessageContentFuzzer.envelope(200),
|
||||
message = DataMessage(pollTerminate = DataMessage.PollTerminate(targetSentTimestamp = 100)),
|
||||
senderRecipient = alice,
|
||||
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
|
||||
threadRecipient = bob,
|
||||
groupId = groupId,
|
||||
receivedTime = 200
|
||||
)
|
||||
|
||||
assert(insertResult?.messageId != null)
|
||||
val poll = SignalDatabase.polls.getPoll(pollMessageId)
|
||||
assert(poll != null)
|
||||
assert(poll!!.hasEnded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollTerminate_whenIHaveDifferentTimestamp_dropMessage() {
|
||||
insertPoll()
|
||||
|
||||
val insertResult = DataMessageProcessor.handlePollTerminate(
|
||||
context = ApplicationProvider.getApplicationContext(),
|
||||
envelope = MessageContentFuzzer.envelope(200),
|
||||
message = DataMessage(pollTerminate = DataMessage.PollTerminate(200)),
|
||||
senderRecipient = alice,
|
||||
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
|
||||
threadRecipient = bob,
|
||||
groupId = groupId,
|
||||
receivedTime = 200
|
||||
)
|
||||
|
||||
assert(insertResult == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollTerminate_whenMessageIsNotFromCreatorOfPoll_dropMessage() {
|
||||
insertPoll()
|
||||
|
||||
val insertResult = DataMessageProcessor.handlePollTerminate(
|
||||
context = ApplicationProvider.getApplicationContext(),
|
||||
envelope = MessageContentFuzzer.envelope(200),
|
||||
message = DataMessage(pollTerminate = DataMessage.PollTerminate(100)),
|
||||
senderRecipient = bob,
|
||||
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
|
||||
threadRecipient = bob,
|
||||
groupId = groupId,
|
||||
receivedTime = 200
|
||||
)
|
||||
|
||||
assert(insertResult == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollTerminate_whenPollDoesNotExist_dropMessage() {
|
||||
val insertResult = DataMessageProcessor.handlePollTerminate(
|
||||
context = ApplicationProvider.getApplicationContext(),
|
||||
envelope = MessageContentFuzzer.envelope(200),
|
||||
message = DataMessage(pollTerminate = DataMessage.PollTerminate(100)),
|
||||
senderRecipient = alice,
|
||||
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
|
||||
threadRecipient = bob,
|
||||
groupId = groupId,
|
||||
receivedTime = 200
|
||||
)
|
||||
|
||||
assert(insertResult == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollVote_whenValidPollVote_processVote() {
|
||||
insertPoll()
|
||||
|
||||
val messageId = handlePollVote(
|
||||
DataMessage.PollVote(
|
||||
targetAuthorAciBinary = alice.asMember().aciBytes,
|
||||
targetSentTimestamp = 100,
|
||||
optionIndexes = listOf(0),
|
||||
voteCount = 1
|
||||
),
|
||||
bob
|
||||
)
|
||||
|
||||
assert(messageId != null)
|
||||
assertThat(messageId!!.id).isEqualTo(1)
|
||||
val poll = SignalDatabase.polls.getPoll(messageId.id)
|
||||
assert(poll != null)
|
||||
assertThat(poll!!.pollOptions[0].voterIds).isEqualTo(listOf(bob.id.toLong()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollVote_whenMultipleVoteAllowed_processAllVote() {
|
||||
insertPoll()
|
||||
|
||||
val messageId = handlePollVote(
|
||||
DataMessage.PollVote(
|
||||
targetAuthorAciBinary = alice.asMember().aciBytes,
|
||||
targetSentTimestamp = 100,
|
||||
optionIndexes = listOf(0, 1, 2),
|
||||
voteCount = 1
|
||||
),
|
||||
bob
|
||||
)
|
||||
|
||||
assert(messageId != null)
|
||||
val poll = SignalDatabase.polls.getPoll(messageId!!.id)
|
||||
assert(poll != null)
|
||||
assertThat(poll!!.pollOptions[0].voterIds).isEqualTo(listOf(bob.id.toLong()))
|
||||
assertThat(poll.pollOptions[1].voterIds).isEqualTo(listOf(bob.id.toLong()))
|
||||
assertThat(poll.pollOptions[2].voterIds).isEqualTo(listOf(bob.id.toLong()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollVote_whenMultipleVoteSentToSingleVotePolls_dropMessage() {
|
||||
insertPoll(false)
|
||||
|
||||
val messageId = handlePollVote(
|
||||
DataMessage.PollVote(
|
||||
targetAuthorAciBinary = alice.asMember().aciBytes,
|
||||
targetSentTimestamp = 100,
|
||||
optionIndexes = listOf(0, 1, 2),
|
||||
voteCount = 1
|
||||
),
|
||||
bob
|
||||
)
|
||||
|
||||
assert(messageId == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollVote_whenVoteCountIsNotHigher_dropMessage() {
|
||||
insertPoll()
|
||||
|
||||
val messageId = handlePollVote(
|
||||
DataMessage.PollVote(
|
||||
targetAuthorAciBinary = alice.asMember().aciBytes,
|
||||
targetSentTimestamp = 100,
|
||||
optionIndexes = listOf(0, 1, 2),
|
||||
voteCount = -1
|
||||
),
|
||||
bob
|
||||
)
|
||||
|
||||
assert(messageId == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollVote_whenVoteOptionDoesNotExist_dropMessage() {
|
||||
insertPoll()
|
||||
|
||||
val messageId = handlePollVote(
|
||||
DataMessage.PollVote(
|
||||
targetAuthorAciBinary = alice.asMember().aciBytes,
|
||||
targetSentTimestamp = 100,
|
||||
optionIndexes = listOf(5),
|
||||
voteCount = 1
|
||||
),
|
||||
bob
|
||||
)
|
||||
|
||||
assert(messageId == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollVote_whenVoterNotInGroup_dropMessage() {
|
||||
insertPoll()
|
||||
|
||||
val messageId = handlePollVote(
|
||||
DataMessage.PollVote(
|
||||
targetAuthorAciBinary = alice.asMember().aciBytes,
|
||||
targetSentTimestamp = 100,
|
||||
optionIndexes = listOf(0, 1, 2),
|
||||
voteCount = 1
|
||||
|
||||
),
|
||||
charlie
|
||||
)
|
||||
|
||||
assert(messageId == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handlePollVote_whenPollDoesNotExist_dropMessage() {
|
||||
val messageId = handlePollVote(
|
||||
DataMessage.PollVote(
|
||||
targetAuthorAciBinary = alice.asMember().aciBytes,
|
||||
targetSentTimestamp = 100,
|
||||
optionIndexes = listOf(0, 1, 2),
|
||||
voteCount = 1
|
||||
),
|
||||
bob
|
||||
)
|
||||
|
||||
assert(messageId == null)
|
||||
}
|
||||
|
||||
private fun handlePollCreate(pollCreate: DataMessage.PollCreate, senderRecipient: Recipient, threadRecipient: Recipient, groupId: GroupId.V2?): MessageTable.InsertResult? {
|
||||
return DataMessageProcessor.handlePollCreate(
|
||||
envelope = MessageContentFuzzer.envelope(100),
|
||||
message = DataMessage(pollCreate = pollCreate),
|
||||
senderRecipient = senderRecipient,
|
||||
threadRecipient = threadRecipient,
|
||||
groupId = groupId,
|
||||
receivedTime = 0,
|
||||
context = ApplicationProvider.getApplicationContext(),
|
||||
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId())
|
||||
)
|
||||
}
|
||||
|
||||
private fun handlePollVote(pollVote: DataMessage.PollVote, senderRecipient: Recipient): MessageId? {
|
||||
return DataMessageProcessor.handlePollVote(
|
||||
context = ApplicationProvider.getApplicationContext(),
|
||||
envelope = MessageContentFuzzer.envelope(100),
|
||||
message = DataMessage(pollVote = pollVote),
|
||||
senderRecipient = senderRecipient,
|
||||
earlyMessageCacheEntry = null
|
||||
)
|
||||
}
|
||||
|
||||
private fun insertPoll(allowMultiple: Boolean = true): Long {
|
||||
val envelope = MessageContentFuzzer.envelope(100)
|
||||
val pollMessage = IncomingMessage(type = MessageType.NORMAL, from = alice.id, sentTimeMillis = envelope.timestamp!!, serverTimeMillis = envelope.serverTimestamp!!, receivedTimeMillis = 0, groupId = groupId)
|
||||
val messageId = SignalDatabase.messages.insertMessageInbox(pollMessage).get()
|
||||
SignalDatabase.polls.insertPoll("question?", allowMultiple, listOf("a", "b", "c"), alice.id.toLong(), messageId.messageId)
|
||||
return messageId.messageId
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.messages.protocol
|
||||
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.libsignal.protocol.ReusedBaseKeyException
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
|
||||
class BufferedKyberPreKeyStoreTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalDatabaseRule()
|
||||
|
||||
private lateinit var aci: ServiceId
|
||||
private lateinit var testSubject: BufferedKyberPreKeyStore
|
||||
private lateinit var dataStore: BufferedSignalServiceAccountDataStore
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||
|
||||
aci = harness.localAci
|
||||
testSubject = BufferedKyberPreKeyStore(aci)
|
||||
dataStore = BufferedSignalServiceAccountDataStore(aci)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenALastResortKey_whenIMarkKyberPreKeyUsed_thenIExpectNoIssues() {
|
||||
KyberPreKeysTestUtil.insertTestRecord(aci, 1, lastResort = true)
|
||||
val publicKey = KyberPreKeysTestUtil.generateECPublicKey()
|
||||
|
||||
testSubject.markKyberPreKeyUsed(
|
||||
kyberPreKeyId = 1,
|
||||
signedPreKeyId = 2,
|
||||
publicKey = publicKey
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = ReusedBaseKeyException::class)
|
||||
fun givenALastResortKey_whenIMarkKyberPreKeyUsedTwice_thenIExpectException() {
|
||||
KyberPreKeysTestUtil.insertTestRecord(aci, 1, lastResort = true)
|
||||
val publicKey = KyberPreKeysTestUtil.generateECPublicKey()
|
||||
|
||||
testSubject.markKyberPreKeyUsed(
|
||||
kyberPreKeyId = 1,
|
||||
signedPreKeyId = 2,
|
||||
publicKey = publicKey
|
||||
)
|
||||
|
||||
testSubject.markKyberPreKeyUsed(
|
||||
kyberPreKeyId = 1,
|
||||
signedPreKeyId = 2,
|
||||
publicKey = publicKey
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAMarkedLastResortKey_whenIFlushTwice_thenIExpectNoIssues() {
|
||||
KyberPreKeysTestUtil.insertTestRecord(aci, 1, lastResort = true)
|
||||
val publicKey = KyberPreKeysTestUtil.generateECPublicKey()
|
||||
|
||||
testSubject.markKyberPreKeyUsed(
|
||||
kyberPreKeyId = 1,
|
||||
signedPreKeyId = 2,
|
||||
publicKey = publicKey
|
||||
)
|
||||
|
||||
testSubject.flushToDisk(dataStore)
|
||||
testSubject.flushToDisk(dataStore)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.billing.BillingPurchaseState
|
||||
import org.signal.core.util.billing.BillingResponseCode
|
||||
import org.signal.core.util.deleteAll
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -94,7 +95,7 @@ class GooglePlayBillingPurchaseTokenMigrationJobTest {
|
||||
)
|
||||
)
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns false
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.BILLING_UNAVAILABLE
|
||||
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
|
||||
@@ -118,7 +119,7 @@ class GooglePlayBillingPurchaseTokenMigrationJobTest {
|
||||
)
|
||||
)
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
|
||||
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.None
|
||||
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
@@ -143,7 +144,7 @@ class GooglePlayBillingPurchaseTokenMigrationJobTest {
|
||||
)
|
||||
)
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
|
||||
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success(
|
||||
purchaseState = BillingPurchaseState.PURCHASED,
|
||||
purchaseToken = "purchaseToken",
|
||||
|
||||
@@ -20,8 +20,10 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.NewAccount
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -125,6 +127,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||
|
||||
SignalStore.settings.isMessageNotificationsEnabled = false
|
||||
SignalStore.registration.restoreDecisionState = RestoreDecisionState.NewAccount
|
||||
|
||||
return Recipient.self()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireLongOrNull
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyPair
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyType
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.thoughtcrime.securesms.database.KyberPreKeyTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.security.SecureRandom
|
||||
|
||||
object KyberPreKeysTestUtil {
|
||||
fun insertTestRecord(account: ServiceId, id: Int, staleTime: Long = 0, lastResort: Boolean = false) {
|
||||
val kemKeyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024)
|
||||
SignalDatabase.kyberPreKeys.insert(
|
||||
serviceId = account,
|
||||
keyId = id,
|
||||
record = KyberPreKeyRecord(
|
||||
id,
|
||||
System.currentTimeMillis(),
|
||||
kemKeyPair,
|
||||
ECKeyPair.generate().privateKey.calculateSignature(kemKeyPair.publicKey.serialize())
|
||||
),
|
||||
lastResort = lastResort
|
||||
)
|
||||
|
||||
val count = SignalDatabase.rawDatabase
|
||||
.update(KyberPreKeyTable.TABLE_NAME)
|
||||
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
|
||||
assertEquals(1, count)
|
||||
}
|
||||
|
||||
fun getStaleTime(account: ServiceId, id: Int): Long? {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select(KyberPreKeyTable.STALE_TIMESTAMP)
|
||||
.from(KyberPreKeyTable.TABLE_NAME)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
|
||||
}
|
||||
|
||||
fun generateECPublicKey(): ECPublicKey {
|
||||
val byteArray = ByteArray(ECPublicKey.KEY_SIZE - 1)
|
||||
SecureRandom().nextBytes(byteArray)
|
||||
|
||||
return ECPublicKey.fromPublicKeyBytes(byteArray)
|
||||
}
|
||||
|
||||
private fun ServiceId.toAccountId(): String {
|
||||
return when (this) {
|
||||
is ACI -> this.toString()
|
||||
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,11 +59,11 @@ object TestMessages {
|
||||
null
|
||||
)
|
||||
if (timestamp != null) {
|
||||
TestDbUtils.setMessageReceived(insert, timestamp)
|
||||
TestDbUtils.setMessageReceived(insert.messageId, timestamp)
|
||||
}
|
||||
SignalDatabase.messages.markAsSent(insert, true)
|
||||
SignalDatabase.messages.markAsSent(insert.messageId, true)
|
||||
|
||||
return insert
|
||||
return insert.messageId
|
||||
}
|
||||
fun insertIncomingTextMessage(other: Recipient, body: String, timestamp: Long? = null) {
|
||||
val message = IncomingMessage(
|
||||
|
||||
@@ -115,6 +115,7 @@ class ConversationElementGenerator {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
null,
|
||||
null,
|
||||
|
||||
@@ -44,6 +44,8 @@ import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.polls.PollOption
|
||||
import org.thoughtcrime.securesms.polls.PollRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
@@ -336,5 +338,17 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
override fun onUpdateSignalClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onViewResultsClicked(pollId: Long) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onViewPollClicked(messageId: Long) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,19 +843,12 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".registrationv3.olddevice.TransferAccountActivity"
|
||||
<activity android:name=".registration.olddevice.TransferAccountActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".registration.ui.RegistrationActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".registrationv3.ui.RegistrationActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize"
|
||||
@@ -930,17 +923,7 @@
|
||||
android:theme="@style/TextSecure.DialogActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".SmsSendtoActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
<activity android:name=".SystemContactsEntrypointActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -991,7 +974,7 @@
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".registrationv3.ui.restore.RemoteRestoreActivity"
|
||||
<activity android:name=".registration.ui.restore.RemoteRestoreActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
|
||||
Binary file not shown.
@@ -20,6 +20,7 @@ import org.signal.glide.common.executor.FrameDecoderExecutor;
|
||||
import org.signal.glide.common.io.Reader;
|
||||
import org.signal.glide.common.io.Writer;
|
||||
import org.signal.glide.common.loader.Loader;
|
||||
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -238,9 +239,10 @@ public abstract class FrameSeqDecoder<R extends Reader, W extends Writer> {
|
||||
return fullRect;
|
||||
}
|
||||
|
||||
private void initCanvasBounds(Rect rect) {
|
||||
private void initCanvasBounds(Rect rect) throws IOException {
|
||||
fullRect = rect;
|
||||
frameBuffer = ByteBuffer.allocate((rect.width() * rect.height() / (sampleSize * sampleSize) + 1) * 4);
|
||||
int capacity = APNGDecoder.getSafeAllocationSize(fullRect.width(), fullRect.height(), sampleSize);
|
||||
frameBuffer = ByteBuffer.allocate(capacity);
|
||||
if (mWriter == null) {
|
||||
mWriter = getWriter();
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
|
||||
private int mLoopCount;
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
public static final int MAX_DIMENSION = 4096;
|
||||
public static final long MAX_TOTAL_PIXELS = 64_000_000L;
|
||||
|
||||
private class SnapShot {
|
||||
byte dispose_op;
|
||||
@@ -126,8 +128,12 @@ public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
|
||||
otherChunks.add(chunk);
|
||||
}
|
||||
}
|
||||
frameBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
snapShot.byteBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
|
||||
|
||||
int capacity = getSafeAllocationSize(canvasWidth, canvasHeight, sampleSize);
|
||||
|
||||
frameBuffer = ByteBuffer.allocate(capacity);
|
||||
snapShot.byteBuffer = ByteBuffer.allocate(capacity);
|
||||
|
||||
return new Rect(0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
@@ -208,4 +214,27 @@ public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
|
||||
Log.e(TAG, "Failed to render!", t);
|
||||
}
|
||||
}
|
||||
|
||||
public static int getSafeAllocationSize(int width, int height, int sampleSize) throws IOException {
|
||||
if (width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
||||
throw new IOException("APNG dimensions exceed safe limits: " + width + "x" + height);
|
||||
}
|
||||
|
||||
int capacity;
|
||||
try {
|
||||
int ss = Math.multiplyExact(sampleSize, sampleSize);
|
||||
int canvasSize = Math.multiplyExact(width, height);
|
||||
|
||||
int pixelCount = canvasSize / ss + 1;
|
||||
if (pixelCount <= 0 || pixelCount > MAX_TOTAL_PIXELS) {
|
||||
throw new IOException("APNG pixel count exceeds safe limits: " + pixelCount);
|
||||
}
|
||||
|
||||
capacity = Math.multiplyExact(pixelCount, 4);
|
||||
} catch (ArithmeticException e) {
|
||||
throw new IOException("Failed to multiply dimensions and sample size: " + width + "x" + height + " @ sample size " + sampleSize + " (overflow?)", e);
|
||||
}
|
||||
|
||||
return capacity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,10 +299,8 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
}
|
||||
|
||||
public void checkFreeDiskSpace() {
|
||||
if (RemoteConfig.messageBackups()) {
|
||||
long availableBytes = BackupRepository.INSTANCE.getFreeStorageSpace().getBytes();
|
||||
SignalStore.backup().setSpaceAvailableOnDiskBytes(availableBytes);
|
||||
}
|
||||
long availableBytes = BackupRepository.INSTANCE.getFreeStorageSpace().getBytes();
|
||||
SignalStore.backup().setSpaceAvailableOnDiskBytes(availableBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,8 @@ import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
|
||||
import org.thoughtcrime.securesms.polls.PollRecord;
|
||||
import org.thoughtcrime.securesms.polls.PollOption;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
@@ -143,5 +145,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onDisplayMediaNoLongerAvailableSheet();
|
||||
void onShowUnverifiedProfileSheet(boolean forGroup);
|
||||
void onUpdateSignalClicked();
|
||||
void onViewResultsClicked(long pollId);
|
||||
void onViewPollClicked(long messageId);
|
||||
void onToggleVote(@NonNull PollRecord poll, @NonNull PollOption pollOption, Boolean isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
|
||||
@@ -106,7 +106,7 @@ fun DevicePinAuthEducationSheet(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun DevicePinAuthEducationSheetPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
|
||||
@@ -30,9 +30,9 @@ import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
|
||||
/**
|
||||
@@ -113,7 +113,7 @@ fun InviteScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun InviteScreenPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -18,12 +18,12 @@ import android.view.ViewTreeObserver
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -37,6 +37,8 @@ import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor
|
||||
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -48,8 +50,8 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import androidx.fragment.compose.rememberFragmentState
|
||||
@@ -74,6 +76,7 @@ import org.signal.donations.StripeApi
|
||||
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.verify.VerifyBackupKeyActivity
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFragment
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
@@ -87,7 +90,6 @@ import org.thoughtcrime.securesms.components.settings.app.notifications.manual.N
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
|
||||
import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay
|
||||
import org.thoughtcrime.securesms.conversation.v2.ShareDataTimestampViewModel
|
||||
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment
|
||||
@@ -100,6 +102,8 @@ import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
|
||||
import org.thoughtcrime.securesms.main.DetailsScreenNavHost
|
||||
import org.thoughtcrime.securesms.main.InsetsViewModelUpdater
|
||||
import org.thoughtcrime.securesms.main.MainBottomChrome
|
||||
import org.thoughtcrime.securesms.main.MainBottomChromeCallback
|
||||
import org.thoughtcrime.securesms.main.MainBottomChromeState
|
||||
@@ -118,6 +122,13 @@ import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
||||
import org.thoughtcrime.securesms.main.NavigationBarSpacerCompat
|
||||
import org.thoughtcrime.securesms.main.SnackbarState
|
||||
import org.thoughtcrime.securesms.main.callNavGraphBuilder
|
||||
import org.thoughtcrime.securesms.main.chatNavGraphBuilder
|
||||
import org.thoughtcrime.securesms.main.navigateToDetailLocation
|
||||
import org.thoughtcrime.securesms.main.rememberDetailNavHostController
|
||||
import org.thoughtcrime.securesms.main.rememberFocusRequester
|
||||
import org.thoughtcrime.securesms.main.rememberMainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.main.storiesNavGraphBuilder
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||
@@ -297,7 +308,6 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
|
||||
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
|
||||
val mainNavigationState by mainNavigationViewModel.mainNavigationState.collectAsStateWithLifecycle()
|
||||
val mainNavigationDetailLocation by mainNavigationViewModel.detailLocation.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(mainNavigationState.currentListLocation) {
|
||||
when (mainNavigationState.currentListLocation) {
|
||||
@@ -308,8 +318,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
}
|
||||
|
||||
val isNavigationVisible = remember(mainToolbarState.mode) {
|
||||
mainToolbarState.mode == MainToolbarMode.FULL
|
||||
val isNavigationVisible = mainToolbarState.mode == MainToolbarMode.FULL
|
||||
val isBackHandlerEnabled = mainToolbarState.destination != MainNavigationListLocation.CHATS
|
||||
|
||||
BackHandler(enabled = isBackHandlerEnabled) {
|
||||
mainNavigationViewModel.setFocusedPane(ThreePaneScaffoldRole.Secondary)
|
||||
mainNavigationViewModel.goTo(MainNavigationListLocation.CHATS)
|
||||
}
|
||||
|
||||
val mainBottomChromeState = remember(mainToolbarState.destination, snackbar, mainToolbarState.mode, megaphone) {
|
||||
@@ -329,8 +343,79 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
|
||||
MainContainer {
|
||||
val wrappedNavigator = rememberNavigator(windowSizeClass, contentLayoutData, maxWidth)
|
||||
val paneExpansionState = rememberPaneExpansionState()
|
||||
val listPaneWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
|
||||
val halfPartitionWidth = contentLayoutData.partitionWidth / 2
|
||||
|
||||
val detailOffset = if (mainToolbarState.mode == MainToolbarMode.SEARCH || mainToolbarState.mode == MainToolbarMode.ACTION_MODE) 0.dp else 72.dp
|
||||
val detailOnlyAnchor = PaneExpansionAnchor.Offset.fromStart(detailOffset + contentLayoutData.listPaddingStart + halfPartitionWidth)
|
||||
val detailAndListAnchor = PaneExpansionAnchor.Offset.fromStart(listPaneWidth + halfPartitionWidth)
|
||||
val listOnlyAnchor = PaneExpansionAnchor.Offset.fromEnd(contentLayoutData.detailPaddingEnd - halfPartitionWidth)
|
||||
|
||||
val paneExpansionState = rememberPaneExpansionState(
|
||||
anchors = listOf(detailOnlyAnchor, detailAndListAnchor, listOnlyAnchor)
|
||||
)
|
||||
|
||||
val mutableInteractionSource = remember { MutableInteractionSource() }
|
||||
val mainNavigationDetailLocation by rememberMainNavigationDetailLocation(mainNavigationViewModel)
|
||||
|
||||
val chatsNavHostController = rememberDetailNavHostController(
|
||||
onRequestFocus = rememberFocusRequester(
|
||||
mainNavigationViewModel = mainNavigationViewModel,
|
||||
currentListLocation = mainNavigationState.currentListLocation,
|
||||
isTargetListLocation = { it in listOf(MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE) }
|
||||
)
|
||||
) {
|
||||
chatNavGraphBuilder()
|
||||
}
|
||||
|
||||
val callsNavHostController = rememberDetailNavHostController(
|
||||
onRequestFocus = rememberFocusRequester(
|
||||
mainNavigationViewModel = mainNavigationViewModel,
|
||||
currentListLocation = mainNavigationState.currentListLocation
|
||||
) { it == MainNavigationListLocation.CALLS }
|
||||
) {
|
||||
callNavGraphBuilder(it)
|
||||
}
|
||||
|
||||
val storiesNavHostController = rememberDetailNavHostController(
|
||||
onRequestFocus = rememberFocusRequester(
|
||||
mainNavigationViewModel = mainNavigationViewModel,
|
||||
currentListLocation = mainNavigationState.currentListLocation
|
||||
) { it == MainNavigationListLocation.STORIES }
|
||||
) {
|
||||
storiesNavGraphBuilder()
|
||||
}
|
||||
|
||||
LaunchedEffect(mainNavigationDetailLocation) {
|
||||
mainNavigationViewModel.clearEarlyDetailLocation()
|
||||
when (mainNavigationDetailLocation) {
|
||||
is MainNavigationDetailLocation.Empty -> {
|
||||
when (mainNavigationState.currentListLocation) {
|
||||
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> chatsNavHostController
|
||||
MainNavigationListLocation.CALLS -> callsNavHostController
|
||||
MainNavigationListLocation.STORIES -> storiesNavHostController
|
||||
}.navigateToDetailLocation(mainNavigationDetailLocation)
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Chats -> chatsNavHostController.navigateToDetailLocation(mainNavigationDetailLocation)
|
||||
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(mainNavigationDetailLocation)
|
||||
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(mainNavigationDetailLocation)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(mainNavigationDetailLocation) {
|
||||
if (paneExpansionState.currentAnchor == listOnlyAnchor && wrappedNavigator.currentDestination?.pane == ThreePaneScaffoldRole.Primary) {
|
||||
paneExpansionState.animateTo(detailOnlyAnchor)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(mainNavigationState.currentListLocation) {
|
||||
if (paneExpansionState.currentAnchor == detailOnlyAnchor && wrappedNavigator.currentDestination?.pane == ThreePaneScaffoldRole.Secondary) {
|
||||
paneExpansionState.animateTo(listOnlyAnchor)
|
||||
}
|
||||
}
|
||||
|
||||
InsetsViewModelUpdater()
|
||||
|
||||
AppScaffold(
|
||||
navigator = wrappedNavigator,
|
||||
@@ -432,35 +517,26 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
}
|
||||
},
|
||||
detailContent = {
|
||||
when (val destination = mainNavigationDetailLocation) {
|
||||
is MainNavigationDetailLocation.Conversation -> {
|
||||
val fragmentState = key(destination) { rememberFragmentState() }
|
||||
AndroidFragment(
|
||||
clazz = ConversationFragment::class.java,
|
||||
fragmentState = fragmentState,
|
||||
arguments = requireNotNull(destination.intent.extras) { "Handed null Conversation intent arguments." },
|
||||
modifier = Modifier
|
||||
.padding(end = contentLayoutData.detailPaddingEnd)
|
||||
.clip(contentLayoutData.shape)
|
||||
.background(color = MaterialTheme.colorScheme.surface)
|
||||
.fillMaxSize()
|
||||
when (mainNavigationState.currentListLocation) {
|
||||
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
|
||||
DetailsScreenNavHost(
|
||||
navHostController = chatsNavHostController,
|
||||
contentLayoutData = contentLayoutData
|
||||
)
|
||||
}
|
||||
|
||||
MainNavigationDetailLocation.Empty -> {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = contentLayoutData.detailPaddingEnd)
|
||||
.clip(contentLayoutData.shape)
|
||||
.background(color = MaterialTheme.colorScheme.surface)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_signal_logo_large),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
MainNavigationListLocation.CALLS -> {
|
||||
DetailsScreenNavHost(
|
||||
navHostController = callsNavHostController,
|
||||
contentLayoutData = contentLayoutData
|
||||
)
|
||||
}
|
||||
|
||||
MainNavigationListLocation.STORIES -> {
|
||||
DetailsScreenNavHost(
|
||||
navHostController = storiesNavHostController,
|
||||
contentLayoutData = contentLayoutData
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -521,8 +597,20 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
return remember(scaffoldNavigator, coroutine) {
|
||||
mainNavigationViewModel.wrapNavigator(coroutine, scaffoldNavigator) { detailLocation ->
|
||||
when (detailLocation) {
|
||||
is MainNavigationDetailLocation.Conversation -> {
|
||||
startActivity(detailLocation.intent)
|
||||
is MainNavigationDetailLocation.Chats.Conversation -> {
|
||||
startActivity(
|
||||
ConversationIntents.createBuilderSync(this, detailLocation.conversationArgs.recipientId, detailLocation.conversationArgs.threadId)
|
||||
.withArgs(detailLocation.conversationArgs)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Calls.CallLinks.CallLinkDetails -> {
|
||||
startActivity(CallLinkDetailsActivity.createIntent(this, detailLocation.callLinkRoomId))
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName -> {
|
||||
error("Unexpected subroute EditCallLinkName.")
|
||||
}
|
||||
|
||||
MainNavigationDetailLocation.Empty -> Unit
|
||||
@@ -744,7 +832,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
||||
private fun handleConversationIntent(intent: Intent) {
|
||||
if (ConversationIntents.isConversationIntent(intent)) {
|
||||
mainNavigationViewModel.goTo(MainNavigationListLocation.CHATS)
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Conversation(intent))
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Chats.Conversation(ConversationIntents.readArgsFromBundle(intent.extras!!)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ public class MainNavigator {
|
||||
Disposable disposable = ConversationIntents.createBuilder(activity, recipientId, threadId)
|
||||
.map(builder -> builder.withDistributionType(distributionType)
|
||||
.withStartingPosition(startingPosition)
|
||||
.build())
|
||||
.subscribe(intent -> viewModel.goTo(new MainNavigationDetailLocation.Conversation(intent)));
|
||||
.toConversationArgs())
|
||||
.subscribe(args -> viewModel.goTo(new MainNavigationDetailLocation.Chats.Conversation(args)));
|
||||
|
||||
lifecycleDisposable.add(disposable);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.thoughtcrime.securesms.restore.RestoreActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -189,7 +188,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
|
||||
private boolean userCanTransferOrRestore() {
|
||||
return !SignalStore.registration().isRegistrationComplete() &&
|
||||
RemoteConfig.restoreAfterRegistration() &&
|
||||
RestoreDecisionStateUtil.isDecisionPending(SignalStore.registration().getRestoreDecisionState());
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ import org.thoughtcrime.securesms.util.Rfc5724Uri;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public class SmsSendtoActivity extends Activity {
|
||||
public class SystemContactsEntrypointActivity extends Activity {
|
||||
|
||||
private static final String TAG = Log.tag(SmsSendtoActivity.class);
|
||||
private static final String TAG = Log.tag(SystemContactsEntrypointActivity.class);
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -32,9 +32,7 @@ public class SmsSendtoActivity extends Activity {
|
||||
private Intent getNextIntent(Intent original) {
|
||||
DestinationAndBody destination;
|
||||
|
||||
if (original.getAction().equals(Intent.ACTION_SENDTO)) {
|
||||
destination = getDestinationForSendTo(original);
|
||||
} else if (original.getData() != null && "content".equals(original.getData().getScheme())) {
|
||||
if (original.getData() != null && "content".equals(original.getData().getScheme())) {
|
||||
destination = getDestinationForSyncAdapter(original);
|
||||
} else {
|
||||
destination = getDestinationForView(original);
|
||||
@@ -64,11 +62,6 @@ public class SmsSendtoActivity extends Activity {
|
||||
return nextIntent;
|
||||
}
|
||||
|
||||
private @NonNull DestinationAndBody getDestinationForSendTo(Intent intent) {
|
||||
return new DestinationAndBody(intent.getData().getSchemeSpecificPart(),
|
||||
intent.getStringExtra("sms_body"));
|
||||
}
|
||||
|
||||
private @NonNull DestinationAndBody getDestinationForView(Intent intent) {
|
||||
try {
|
||||
Rfc5724Uri smsUri = new Rfc5724Uri(intent.getData().toString());
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms.audio;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioRecord;
|
||||
import android.media.MediaCodec;
|
||||
@@ -191,6 +192,7 @@ public class AudioCodec implements Recorder {
|
||||
return adtsHeader;
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private AudioRecord createAudioRecord(int bufferSize) {
|
||||
return new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Renders Avatar objects into Media objects. This can involve creating a Bitmap, depending on the
|
||||
@@ -132,6 +131,20 @@ object AvatarRenderer {
|
||||
}
|
||||
|
||||
private fun createMedia(uri: Uri, size: Long): Media {
|
||||
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())
|
||||
return Media(
|
||||
uri = uri,
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
date = System.currentTimeMillis(),
|
||||
width = DIMENSIONS,
|
||||
height = DIMENSIONS,
|
||||
size = size,
|
||||
duration = 0,
|
||||
isBorderless = false,
|
||||
isVideoGif = false,
|
||||
bucketId = null,
|
||||
caption = null,
|
||||
transformProperties = null,
|
||||
fileName = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
@@ -94,7 +94,7 @@ fun FallbackAvatarImage(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun FallbackAvatarImagePreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.avatar.text.TextAvatarCreationFragment
|
||||
import org.thoughtcrime.securesms.avatar.vector.VectorAvatarCreationFragment
|
||||
import org.thoughtcrime.securesms.components.ButtonStripItemView
|
||||
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||
@@ -57,9 +56,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
|
||||
private fun createFactory(): AvatarPickerViewModel.Factory {
|
||||
val args = AvatarPickerFragmentArgs.fromBundle(requireArguments())
|
||||
val groupId = ParcelableGroupId.get(args.groupId)
|
||||
|
||||
return AvatarPickerViewModel.Factory(AvatarPickerRepository(requireContext()), groupId, args.isNewGroup, args.groupAvatarMedia)
|
||||
return AvatarPickerViewModel.Factory(AvatarPickerRepository(requireContext()), args.groupId, args.isNewGroup, args.groupAvatarMedia)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -168,8 +168,9 @@ object ArchiveUploadProgress {
|
||||
|
||||
fun onAttachmentSectionStarted(totalAttachmentBytes: Long) {
|
||||
debugAttachmentStartTime = System.currentTimeMillis()
|
||||
attachmentProgress.clear()
|
||||
updateState {
|
||||
it.copy(
|
||||
ArchiveUploadProgressState(
|
||||
state = ArchiveUploadProgressState.State.UploadMedia,
|
||||
mediaUploadedBytes = 0,
|
||||
mediaTotalBytes = totalAttachmentBytes
|
||||
@@ -203,15 +204,24 @@ object ArchiveUploadProgress {
|
||||
}
|
||||
|
||||
fun onMessageBackupFinishedEarly() {
|
||||
updateState { PROGRESS_NONE }
|
||||
resetState()
|
||||
}
|
||||
|
||||
fun onValidationFailure() {
|
||||
updateState { PROGRESS_NONE }
|
||||
resetState()
|
||||
}
|
||||
|
||||
fun onMainBackupFileUploadFailure() {
|
||||
updateState { PROGRESS_NONE }
|
||||
resetState()
|
||||
}
|
||||
|
||||
private fun resetState() {
|
||||
val shouldRevertToUploadMedia = SignalStore.backup.backsUpMedia && !AppDependencies.jobManager.areQueuesEmpty(UploadAttachmentToArchiveJob.QUEUES)
|
||||
if (shouldRevertToUploadMedia) {
|
||||
onAttachmentSectionStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes())
|
||||
} else {
|
||||
updateState { PROGRESS_NONE }
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateState(
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.signal.core.util.PendingIntentFlags;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper;
|
||||
@@ -26,6 +30,7 @@ public enum BackupFileIOError {
|
||||
ATTACHMENT_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_contains_a_very_large_file),
|
||||
UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups);
|
||||
|
||||
private static final String TAG = Log.tag(BackupFileIOError.class);
|
||||
private static final short BACKUP_FAILED_ID = 31321;
|
||||
|
||||
private final @StringRes int titleId;
|
||||
@@ -41,6 +46,11 @@ public enum BackupFileIOError {
|
||||
}
|
||||
|
||||
public void postNotification(@NonNull Context context) {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.w(TAG, "postNotification: Notification permission is not granted.");
|
||||
return;
|
||||
}
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), PendingIntentFlags.mutable());
|
||||
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
|
||||
.setSmallIcon(R.drawable.ic_signal_backup)
|
||||
|
||||
@@ -119,6 +119,10 @@ object ExportSkips {
|
||||
return log(sentTimestamp, "Failed to parse thread merge event.")
|
||||
}
|
||||
|
||||
fun individualChatUpdateInWrongTypeOfChat(sentTimestamp: Long): String {
|
||||
return log(sentTimestamp, "A chat update that only makes sense for individual chats was found in a different kind of chat.")
|
||||
}
|
||||
|
||||
private fun log(sentTimestamp: Long, message: String): String {
|
||||
return "[SKIP][$sentTimestamp] $message"
|
||||
}
|
||||
@@ -175,7 +179,7 @@ object ExportOddities {
|
||||
}
|
||||
|
||||
fun unreadableLongTextAttachment(sentTimestamp: Long): String {
|
||||
return log(sentTimestamp, "Long text attachment was unreadable. Falling back to the known body with an attachment pointer.")
|
||||
return log(sentTimestamp, "Long text attachment was unreadable. Dropping the pointer.")
|
||||
}
|
||||
|
||||
fun unopenableLongTextAttachment(sentTimestamp: Long): String {
|
||||
|
||||
@@ -169,6 +169,18 @@ object ArchiveRestoreProgress {
|
||||
val remainingRestoreSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize().bytes
|
||||
var restoreState = SignalStore.backup.restoreState
|
||||
|
||||
if (restoreState.isMediaRestoreOperation) {
|
||||
if (remainingRestoreSize == 0.bytes && SignalStore.backup.totalRestorableAttachmentSize == 0L) {
|
||||
restoreState = RestoreState.NONE
|
||||
SignalStore.backup.restoreState = restoreState
|
||||
unregisterUpdateListeners()
|
||||
} else {
|
||||
registerUpdateListeners()
|
||||
}
|
||||
} else {
|
||||
unregisterUpdateListeners()
|
||||
}
|
||||
|
||||
val status = when {
|
||||
!WifiConstraint.isMet(AppDependencies.application) && !SignalStore.backup.restoreWithCellular -> ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_WIFI
|
||||
!NetworkConstraint.isMet(AppDependencies.application) -> ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_INTERNET
|
||||
@@ -185,17 +197,6 @@ object ArchiveRestoreProgress {
|
||||
}
|
||||
}
|
||||
|
||||
if (restoreState.isMediaRestoreOperation) {
|
||||
if (remainingRestoreSize == 0.bytes && SignalStore.backup.totalRestorableAttachmentSize == 0L) {
|
||||
restoreState = RestoreState.NONE
|
||||
SignalStore.backup.restoreState = restoreState
|
||||
} else {
|
||||
registerUpdateListeners()
|
||||
}
|
||||
} else {
|
||||
unregisterUpdateListeners()
|
||||
}
|
||||
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize.bytes
|
||||
|
||||
state.copy(
|
||||
@@ -203,7 +204,7 @@ object ArchiveRestoreProgress {
|
||||
remainingRestoreSize = remainingRestoreSize,
|
||||
restoreStatus = status,
|
||||
totalRestoreSize = totalRestoreSize,
|
||||
hasActivelyRestoredThisRun = state.hasActivelyRestoredThisRun || SignalStore.backup.totalRestorableAttachmentSize > 0,
|
||||
hasActivelyRestoredThisRun = state.hasActivelyRestoredThisRun || totalRestoreSize > 0.bytes,
|
||||
totalToRestoreThisRun = if (totalRestoreSize > 0.bytes) totalRestoreSize else state.totalToRestoreThisRun
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ package org.thoughtcrime.securesms.backup.v2
|
||||
import org.signal.core.util.ByteSize
|
||||
import org.signal.core.util.bytes
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* In-memory view of the current state of an attachment restore process.
|
||||
@@ -24,13 +26,15 @@ data class ArchiveRestoreProgressState(
|
||||
|
||||
val progress: Float? = when (this.restoreState) {
|
||||
RestoreState.CALCULATING_MEDIA,
|
||||
RestoreState.CANCELING_MEDIA -> this.completedRestoredSize.percentageOf(this.totalRestoreSize)
|
||||
RestoreState.CANCELING_MEDIA -> {
|
||||
max(0f, min(1f, this.completedRestoredSize.percentageOf(this.totalRestoreSize)))
|
||||
}
|
||||
|
||||
RestoreState.RESTORING_MEDIA -> {
|
||||
when (this.restoreStatus) {
|
||||
RestoreStatus.NONE -> null
|
||||
RestoreStatus.FINISHED -> 1f
|
||||
else -> this.completedRestoredSize.percentageOf(this.totalRestoreSize)
|
||||
else -> max(0f, min(1f, this.completedRestoredSize.percentageOf(this.totalRestoreSize)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ import org.thoughtcrime.securesms.backup.v2.processor.RecipientArchiveProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.StickerArchiveProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupDebugInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.BackupImportReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
|
||||
@@ -80,6 +81,7 @@ import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.backup.v2.util.ArchiveAttachmentInfo
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
@@ -107,10 +109,14 @@ import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.CancelRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||
import org.thoughtcrime.securesms.jobs.ResetSvrGuessCountJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
|
||||
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
|
||||
import org.thoughtcrime.securesms.jobs.Svr2MirrorJob
|
||||
import org.thoughtcrime.securesms.jobs.UploadAttachmentToArchiveJob
|
||||
import org.thoughtcrime.securesms.keyvalue.BackupValues.ArchiveServiceCredentials
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||
@@ -129,6 +135,7 @@ import org.thoughtcrime.securesms.service.BackupProgressService
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.toMillis
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
import org.whispersystems.signalservice.api.ApplicationErrorAction
|
||||
@@ -162,6 +169,7 @@ import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.math.BigDecimal
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
@@ -236,9 +244,22 @@ object BackupRepository {
|
||||
resetInitializedStateAndAuthCredentials()
|
||||
SignalStore.account.rotateAccountEntropyPool(stagedKeyRotations.aep)
|
||||
SignalStore.backup.mediaRootBackupKey = stagedKeyRotations.mediaRootBackupKey
|
||||
refreshMasterKeyDependents()
|
||||
BackupMessagesJob.enqueue()
|
||||
}
|
||||
|
||||
private fun refreshMasterKeyDependents() {
|
||||
val jobs = buildList {
|
||||
add(Svr2MirrorJob())
|
||||
if (SignalStore.account.isMultiDevice) {
|
||||
add(MultiDeviceKeysUpdateJob())
|
||||
}
|
||||
add(StorageForcePushJob())
|
||||
}
|
||||
|
||||
AppDependencies.jobManager.addAll(jobs)
|
||||
}
|
||||
|
||||
fun resetInitializedStateAndAuthCredentials() {
|
||||
SignalStore.backup.backupsInitialized = false
|
||||
SignalStore.backup.messageCredentials.clearAll()
|
||||
@@ -541,7 +562,39 @@ object BackupRepository {
|
||||
return false
|
||||
}
|
||||
|
||||
return SignalStore.backup.hasBackupBeenUploaded && System.currentTimeMillis().milliseconds > SignalStore.backup.nextBackupFailureSheetSnoozeTime
|
||||
val isRegistered = SignalStore.account.isRegistered && !TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application)
|
||||
if (!isRegistered) {
|
||||
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet for unregistered user.")
|
||||
return false
|
||||
}
|
||||
|
||||
if (SignalStore.backup.lastBackupTime <= 0) {
|
||||
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet as the last backup time is unset.")
|
||||
return false
|
||||
}
|
||||
|
||||
if (!SignalStore.backup.hasBackupBeenUploaded) {
|
||||
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet as a backup has never been uploaded.")
|
||||
return false
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis().milliseconds
|
||||
val lastBackupTime = SignalStore.backup.lastBackupTime.milliseconds
|
||||
val nextSnoozeTime = SignalStore.backup.nextBackupFailureSnoozeTime
|
||||
|
||||
val isLastBackupTimeAtLeastAWeekAgo = now - 7.days > lastBackupTime
|
||||
if (!isLastBackupTimeAtLeastAWeekAgo) {
|
||||
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet as the last backup time is less than a week ago.")
|
||||
return false
|
||||
}
|
||||
|
||||
val isNextSnoozeTimeBeforeNow = nextSnoozeTime < now
|
||||
if (!isNextSnoozeTimeBeforeNow) {
|
||||
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet as the next snooze time is in the future.")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun snoozeDownloadYourBackupData() {
|
||||
@@ -556,7 +609,14 @@ object BackupRepository {
|
||||
return
|
||||
}
|
||||
|
||||
if (!SignalStore.backup.backsUpMedia || !AppDependencies.jobManager.areQueuesEmpty(UploadAttachmentToArchiveJob.QUEUES)) {
|
||||
if (!SignalStore.backup.backsUpMedia) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!AppDependencies.jobManager.areQueuesEmpty(UploadAttachmentToArchiveJob.QUEUES)) {
|
||||
if (SignalStore.backup.archiveUploadState?.state == ArchiveUploadProgressState.State.None) {
|
||||
ArchiveUploadProgress.onAttachmentSectionStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -607,7 +667,7 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
private fun shouldNotDisplayBackupFailedMessaging(): Boolean {
|
||||
return !SignalStore.account.isRegistered || !RemoteConfig.messageBackups || !SignalStore.backup.areBackupsEnabled
|
||||
return !SignalStore.account.isRegistered || !SignalStore.backup.areBackupsEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -710,13 +770,18 @@ object BackupRepository {
|
||||
append = { main.write(it) }
|
||||
)
|
||||
|
||||
val maxBufferSize = 10_000
|
||||
var totalAttachmentCount = 0
|
||||
val attachmentInfos: MutableSet<ArchiveAttachmentInfo> = mutableSetOf()
|
||||
|
||||
export(
|
||||
currentTime = System.currentTimeMillis(),
|
||||
isLocal = true,
|
||||
writer = writer,
|
||||
progressEmitter = localBackupProgressEmitter,
|
||||
cancellationSignal = cancellationSignal,
|
||||
forTransfer = false
|
||||
forTransfer = false,
|
||||
extraFrameOperation = null
|
||||
) { dbSnapshot ->
|
||||
val localArchivableAttachments = dbSnapshot
|
||||
.attachmentTable
|
||||
@@ -752,7 +817,7 @@ object BackupRepository {
|
||||
currentTime: Long,
|
||||
progressEmitter: ExportProgressListener? = null,
|
||||
cancellationSignal: () -> Boolean = { false },
|
||||
extraExportOperations: ((SignalDatabase) -> Unit)?
|
||||
extraFrameOperation: ((Frame) -> Unit)?
|
||||
) {
|
||||
val writer = EncryptedBackupWriter.createForSignalBackup(
|
||||
key = messageBackupKey,
|
||||
@@ -770,7 +835,8 @@ object BackupRepository {
|
||||
forTransfer = false,
|
||||
progressEmitter = progressEmitter,
|
||||
cancellationSignal = cancellationSignal,
|
||||
extraExportOperations = extraExportOperations
|
||||
extraFrameOperation = extraFrameOperation,
|
||||
endingExportOperation = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -799,7 +865,8 @@ object BackupRepository {
|
||||
forTransfer = true,
|
||||
progressEmitter = progressEmitter,
|
||||
cancellationSignal = cancellationSignal,
|
||||
extraExportOperations = null
|
||||
extraFrameOperation = null,
|
||||
endingExportOperation = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -813,8 +880,7 @@ object BackupRepository {
|
||||
currentTime: Long = System.currentTimeMillis(),
|
||||
forTransfer: Boolean = false,
|
||||
progressEmitter: ExportProgressListener? = null,
|
||||
cancellationSignal: () -> Boolean = { false },
|
||||
extraExportOperations: ((SignalDatabase) -> Unit)? = null
|
||||
cancellationSignal: () -> Boolean = { false }
|
||||
) {
|
||||
val writer: BackupExportWriter = if (plaintext) {
|
||||
PlainTextBackupWriter(outputStream)
|
||||
@@ -834,7 +900,8 @@ object BackupRepository {
|
||||
forTransfer = forTransfer,
|
||||
progressEmitter = progressEmitter,
|
||||
cancellationSignal = cancellationSignal,
|
||||
extraExportOperations = extraExportOperations
|
||||
extraFrameOperation = null,
|
||||
endingExportOperation = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -856,7 +923,8 @@ object BackupRepository {
|
||||
forTransfer: Boolean,
|
||||
progressEmitter: ExportProgressListener?,
|
||||
cancellationSignal: () -> Boolean,
|
||||
extraExportOperations: ((SignalDatabase) -> Unit)?
|
||||
extraFrameOperation: ((Frame) -> Unit)?,
|
||||
endingExportOperation: ((SignalDatabase) -> Unit)?
|
||||
) {
|
||||
val eventTimer = EventTimer()
|
||||
val mainDbName = if (isLocal) LOCAL_MAIN_DB_SNAPSHOT_NAME else REMOTE_MAIN_DB_SNAPSHOT_NAME
|
||||
@@ -894,8 +962,9 @@ object BackupRepository {
|
||||
// We're using a snapshot, so the transaction is more for perf than correctness
|
||||
dbSnapshot.rawWritableDatabase.withinTransaction {
|
||||
progressEmitter?.onAccount()
|
||||
AccountDataArchiveProcessor.export(dbSnapshot, signalStoreSnapshot) {
|
||||
writer.write(it)
|
||||
AccountDataArchiveProcessor.export(dbSnapshot, signalStoreSnapshot) { frame ->
|
||||
writer.write(frame)
|
||||
extraFrameOperation?.invoke(frame)
|
||||
eventTimer.emit("account")
|
||||
frameCount++
|
||||
}
|
||||
@@ -907,6 +976,7 @@ object BackupRepository {
|
||||
progressEmitter?.onRecipient()
|
||||
RecipientArchiveProcessor.export(dbSnapshot, signalStoreSnapshot, exportState, selfRecipientId, selfAci) {
|
||||
writer.write(it)
|
||||
extraFrameOperation?.invoke(it)
|
||||
eventTimer.emit("recipient")
|
||||
frameCount++
|
||||
}
|
||||
@@ -918,6 +988,7 @@ object BackupRepository {
|
||||
progressEmitter?.onThread()
|
||||
ChatArchiveProcessor.export(dbSnapshot, exportState) { frame ->
|
||||
writer.write(frame)
|
||||
extraFrameOperation?.invoke(frame)
|
||||
eventTimer.emit("thread")
|
||||
frameCount++
|
||||
}
|
||||
@@ -928,6 +999,7 @@ object BackupRepository {
|
||||
progressEmitter?.onCall()
|
||||
AdHocCallArchiveProcessor.export(dbSnapshot, exportState) { frame ->
|
||||
writer.write(frame)
|
||||
extraFrameOperation?.invoke(frame)
|
||||
eventTimer.emit("call")
|
||||
frameCount++
|
||||
}
|
||||
@@ -939,6 +1011,7 @@ object BackupRepository {
|
||||
progressEmitter?.onSticker()
|
||||
StickerArchiveProcessor.export(dbSnapshot) { frame ->
|
||||
writer.write(frame)
|
||||
extraFrameOperation?.invoke(frame)
|
||||
eventTimer.emit("sticker-pack")
|
||||
frameCount++
|
||||
}
|
||||
@@ -950,6 +1023,7 @@ object BackupRepository {
|
||||
progressEmitter?.onNotificationProfile()
|
||||
NotificationProfileProcessor.export(dbSnapshot, exportState) { frame ->
|
||||
writer.write(frame)
|
||||
extraFrameOperation?.invoke(frame)
|
||||
eventTimer.emit("notification-profile")
|
||||
frameCount++
|
||||
}
|
||||
@@ -961,6 +1035,7 @@ object BackupRepository {
|
||||
progressEmitter?.onChatFolder()
|
||||
ChatFolderProcessor.export(dbSnapshot, exportState) { frame ->
|
||||
writer.write(frame)
|
||||
extraFrameOperation?.invoke(frame)
|
||||
eventTimer.emit("chat-folder")
|
||||
frameCount++
|
||||
}
|
||||
@@ -974,6 +1049,7 @@ object BackupRepository {
|
||||
progressEmitter?.onMessage(0, approximateMessageCount)
|
||||
ChatItemArchiveProcessor.export(dbSnapshot, exportState, selfRecipientId, cancellationSignal) { frame ->
|
||||
writer.write(frame)
|
||||
extraFrameOperation?.invoke(frame)
|
||||
eventTimer.emit("message")
|
||||
frameCount++
|
||||
|
||||
@@ -989,7 +1065,7 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
extraExportOperations?.invoke(dbSnapshot)
|
||||
endingExportOperation?.invoke(dbSnapshot)
|
||||
|
||||
Log.d(TAG, "[export] totalFrames: $frameCount | ${eventTimer.stop().summary}")
|
||||
} finally {
|
||||
@@ -1334,6 +1410,10 @@ object BackupRepository {
|
||||
AppDependencies.recipientCache.warmUp()
|
||||
SignalDatabase.threads.clearCache()
|
||||
|
||||
if (SignalStore.svr.pin?.isNotBlank() == true) {
|
||||
AppDependencies.jobManager.add(ResetSvrGuessCountJob())
|
||||
}
|
||||
|
||||
val stickerJobs = SignalDatabase.stickers.getAllStickerPacks().use { cursor ->
|
||||
val reader = StickerTable.StickerPackRecordReader(cursor)
|
||||
reader
|
||||
@@ -1738,7 +1818,7 @@ object BackupRepository {
|
||||
return RestoreTimestampResult.Success(SignalStore.backup.lastBackupTime)
|
||||
}
|
||||
|
||||
timestampResult is NetworkResult.StatusCodeError && timestampResult.code == 404 -> {
|
||||
timestampResult is NetworkResult.StatusCodeError && (timestampResult.code == 401 || timestampResult.code == 404) -> {
|
||||
Log.i(TAG, "No backup file exists")
|
||||
SignalStore.backup.lastBackupTime = 0L
|
||||
SignalStore.backup.isBackupTimestampRestored = true
|
||||
@@ -1813,7 +1893,7 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAvailableBackupsTypes(availableBackupTiers: List<MessageBackupTier>): List<MessageBackupsType> {
|
||||
suspend fun getBackupTypes(availableBackupTiers: List<MessageBackupTier>): List<MessageBackupsType> {
|
||||
return availableBackupTiers.mapNotNull {
|
||||
val type = getBackupsType(it)
|
||||
|
||||
@@ -1830,8 +1910,9 @@ object BackupRepository {
|
||||
|
||||
@WorkerThread
|
||||
fun getBackupLevelConfiguration(): NetworkResult<SubscriptionsConfiguration.BackupLevelConfiguration> {
|
||||
return AppDependencies.donationsApi
|
||||
return AppDependencies.donationsService
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
.toNetworkResult()
|
||||
.then {
|
||||
val config = it.backupConfiguration.backupLevelConfigurationMap[SubscriptionsConfiguration.BACKUPS_LEVEL]
|
||||
if (config != null) {
|
||||
@@ -1844,8 +1925,9 @@ object BackupRepository {
|
||||
|
||||
@WorkerThread
|
||||
fun getFreeType(): NetworkResult<MessageBackupsType.Free> {
|
||||
return AppDependencies.donationsApi
|
||||
return AppDependencies.donationsService
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
.toNetworkResult()
|
||||
.map {
|
||||
MessageBackupsType.Free(
|
||||
mediaRetentionDays = it.backupConfiguration.freeTierMediaDays
|
||||
@@ -1859,9 +1941,11 @@ object BackupRepository {
|
||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()?.activeSubscription?.let {
|
||||
FiatMoney.fromSignalNetworkAmount(it.amount, Currency.getInstance(it.currency))
|
||||
}
|
||||
} else {
|
||||
} else if (AppDependencies.billingApi.getApiAvailability().isSuccess) {
|
||||
Log.d(TAG, "Accessing price via billing api.")
|
||||
AppDependencies.billingApi.queryProduct()?.price
|
||||
} else {
|
||||
FiatMoney(BigDecimal.ZERO, SignalStore.inAppPayments.getRecurringDonationCurrency())
|
||||
}
|
||||
|
||||
if (productPrice == null) {
|
||||
@@ -1895,15 +1979,14 @@ object BackupRepository {
|
||||
* prevents early initialization with incorrect keys before we have restored them.
|
||||
*/
|
||||
private fun initBackupAndFetchAuth(): NetworkResult<ArchiveServiceAccessPair> {
|
||||
return if (!RemoteConfig.messageBackups) {
|
||||
NetworkResult.StatusCodeError(555, null, null, emptyMap(), NonSuccessfulResponseCodeException(555, "Backups disabled!"))
|
||||
} else if (SignalStore.backup.backupsInitialized || SignalStore.account.isLinkedDevice) {
|
||||
return if (SignalStore.backup.backupsInitialized || SignalStore.account.isLinkedDevice) {
|
||||
getArchiveServiceAccessPair()
|
||||
.runOnStatusCodeError(resetInitializedStateErrorAction)
|
||||
.runOnApplicationError(clearAuthCredentials)
|
||||
} else if (isPreRestoreDuringRegistration()) {
|
||||
Log.w(TAG, "Requesting/using auth credentials in pre-restore state", Throwable())
|
||||
getArchiveServiceAccessPair()
|
||||
.runOnApplicationError(clearAuthCredentials)
|
||||
} else {
|
||||
val messageBackupKey = SignalStore.backup.messageBackupKey
|
||||
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
|
||||
@@ -1955,8 +2038,7 @@ object BackupRepository {
|
||||
|
||||
private fun isPreRestoreDuringRegistration(): Boolean {
|
||||
return !SignalStore.registration.isRegistrationComplete &&
|
||||
SignalStore.registration.restoreDecisionState.isDecisionPending &&
|
||||
RemoteConfig.restoreAfterRegistration
|
||||
SignalStore.registration.restoreDecisionState.isDecisionPending
|
||||
}
|
||||
|
||||
private fun scheduleSyncForAccountChange() {
|
||||
@@ -2051,7 +2133,7 @@ object BackupRepository {
|
||||
val messageBackupKey = SignalStore.backup.messageBackupKey
|
||||
|
||||
Log.i(TAG, "[remoteRestore] Fetching SVRB data")
|
||||
val svrBAuth = when (val result = BackupRepository.getSvrBAuth()) {
|
||||
val svrBAuth = when (val result = getSvrBAuth()) {
|
||||
is NetworkResult.Success -> result.result
|
||||
is NetworkResult.NetworkError -> return RemoteRestoreResult.NetworkError.logW(TAG, "[remoteRestore] Network error when getting SVRB auth.", result.getCause())
|
||||
is NetworkResult.StatusCodeError -> return RemoteRestoreResult.NetworkError.logW(TAG, "[remoteRestore] Status code error when getting SVRB auth.", result.getCause())
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable.Companion.DATE_RECEIVED
|
||||
import org.thoughtcrime.securesms.database.MessageTable.Companion.EXPIRES_IN
|
||||
import org.thoughtcrime.securesms.database.MessageTable.Companion.PARENT_STORY_ID
|
||||
import org.thoughtcrime.securesms.database.MessageTable.Companion.SCHEDULED_DATE
|
||||
import org.thoughtcrime.securesms.database.MessageTable.Companion.STORY_TYPE
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
@@ -66,7 +67,7 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
|
||||
${MessageTable.MESSAGE_EXTRAS},
|
||||
${MessageTable.VIEW_ONCE}
|
||||
)
|
||||
WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0
|
||||
WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1
|
||||
""".trimMargin()
|
||||
)
|
||||
Log.d(TAG, "Creating index took ${System.currentTimeMillis() - startTime} ms")
|
||||
@@ -133,7 +134,7 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
|
||||
PARENT_STORY_ID
|
||||
)
|
||||
.from("${MessageTable.TABLE_NAME} INDEXED BY $dateReceivedIndex")
|
||||
.where("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND ($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds}) AND $DATE_RECEIVED >= $lastSeenReceivedTime")
|
||||
.where("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND ($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds}) AND $DATE_RECEIVED >= $lastSeenReceivedTime")
|
||||
.limit(count)
|
||||
.orderBy("$DATE_RECEIVED ASC")
|
||||
.run()
|
||||
|
||||
@@ -398,7 +398,7 @@ class ChatItemArchiveExporter(
|
||||
|
||||
if (record.latestRevisionId == null) {
|
||||
builder.revisions = revisionMap.remove(record.id)?.repairRevisions(builder) ?: emptyList()
|
||||
val chatItem = builder.build().validateChatItem() ?: continue
|
||||
val chatItem = builder.build().validateChatItem(exportState) ?: continue
|
||||
buffer += chatItem
|
||||
} else {
|
||||
var previousEdits = revisionMap[record.latestRevisionId]
|
||||
@@ -549,7 +549,7 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: Recipien
|
||||
dateReceived = dateReceived
|
||||
)
|
||||
|
||||
if (expiresInMs != null && outgoing?.sendStatus?.all { it.pending == null && it.failed == null } == true) {
|
||||
if (expiresInMs != null && outgoing?.sendStatus?.all { it.pending == null && it.failed == null } == true && expireStartDate == null) {
|
||||
Log.w(TAG, ExportOddities.outgoingMessageWasSentButTimerNotStarted(record.dateSent))
|
||||
expireStartDate = record.dateReceived
|
||||
}
|
||||
@@ -1041,19 +1041,22 @@ private fun BackupMessageRecord.getBodyText(attachments: List<DatabaseAttachment
|
||||
}
|
||||
|
||||
if (longTextAttachment.uri == null || longTextAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE) {
|
||||
return StringUtil.trimToFit(this.body.emptyIfNull(), MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER) to longTextAttachment
|
||||
Log.w(TAG, ExportOddities.undownloadedLongTextAttachment(this.dateSent))
|
||||
val body = StringUtil.trimToFit(this.body.emptyIfNull(), MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER)
|
||||
return body to longTextAttachment.takeUnless { body.isBlank() }
|
||||
}
|
||||
|
||||
val longText = try {
|
||||
PartAuthority.getAttachmentStream(AppDependencies.application, longTextAttachment.uri!!)?.readFully()?.toString(Charsets.UTF_8)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, ExportOddities.unreadableLongTextAttachment(this.dateSent))
|
||||
return this.body.emptyIfNull() to longTextAttachment
|
||||
return this.body.emptyIfNull() to null
|
||||
}
|
||||
|
||||
if (longText == null) {
|
||||
Log.w(TAG, ExportOddities.unopenableLongTextAttachment(this.dateSent))
|
||||
return StringUtil.trimToFit(this.body.emptyIfNull(), MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER) to longTextAttachment
|
||||
val body = StringUtil.trimToFit(this.body.emptyIfNull(), MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER)
|
||||
return body to longTextAttachment.takeUnless { body.isBlank() }
|
||||
}
|
||||
|
||||
val trimmed = StringUtil.trimToFit(longText, MAX_INLINED_BODY_SIZE)
|
||||
@@ -1376,7 +1379,7 @@ private fun List<GroupReceiptTable.GroupReceiptInfo>?.toRemoteSendStatus(message
|
||||
reason = SendStatus.Failed.FailureReason.NETWORK
|
||||
)
|
||||
}
|
||||
MessageTypes.isFailedMessageType(messageRecord.type) -> {
|
||||
it.status == GroupReceiptTable.STATUS_FAILED -> {
|
||||
statusBuilder.failed = SendStatus.Failed(
|
||||
reason = SendStatus.Failed.FailureReason.UNKNOWN
|
||||
)
|
||||
@@ -1510,7 +1513,7 @@ private fun <T> ExecutorService.submitTyped(callable: Callable<T>): Future<T> {
|
||||
return this.submit(callable)
|
||||
}
|
||||
|
||||
fun ChatItem.validateChatItem(): ChatItem? {
|
||||
private fun ChatItem.validateChatItem(exportState: ExportState): ChatItem? {
|
||||
if (this.standardMessage == null &&
|
||||
this.contactMessage == null &&
|
||||
this.stickerMessage == null &&
|
||||
@@ -1524,10 +1527,24 @@ fun ChatItem.validateChatItem(): ChatItem? {
|
||||
Log.w(TAG, ExportSkips.emptyChatItem(this.dateSent))
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.updateMessage != null && this.updateMessage.isOnlyForIndividualChats() && exportState.threadIdToRecipientId[this.chatId] !in exportState.contactRecipientIds) {
|
||||
Log.w(TAG, ExportSkips.individualChatUpdateInWrongTypeOfChat(this.dateSent))
|
||||
return null
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun List<ChatItem>.repairRevisions(current: ChatItem.Builder): List<ChatItem> {
|
||||
private fun ChatUpdateMessage.isOnlyForIndividualChats(): Boolean {
|
||||
return this.simpleUpdate?.type == SimpleChatUpdate.Type.JOINED_SIGNAL ||
|
||||
this.simpleUpdate?.type == SimpleChatUpdate.Type.END_SESSION ||
|
||||
this.simpleUpdate?.type == SimpleChatUpdate.Type.CHAT_SESSION_REFRESH ||
|
||||
this.simpleUpdate?.type == SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST ||
|
||||
this.simpleUpdate?.type == SimpleChatUpdate.Type.PAYMENTS_ACTIVATED
|
||||
}
|
||||
|
||||
private fun List<ChatItem>.repairRevisions(current: ChatItem.Builder): List<ChatItem> {
|
||||
return if (current.standardMessage != null) {
|
||||
val filtered = this
|
||||
.filter { it.standardMessage != null }
|
||||
|
||||
@@ -1073,7 +1073,7 @@ class ChatItemArchiveImporter(
|
||||
this.read != null -> GroupReceiptTable.STATUS_READ
|
||||
this.viewed != null -> GroupReceiptTable.STATUS_VIEWED
|
||||
this.skipped != null -> GroupReceiptTable.STATUS_SKIPPED
|
||||
this.failed != null -> GroupReceiptTable.STATUS_UNKNOWN
|
||||
this.failed != null -> GroupReceiptTable.STATUS_FAILED
|
||||
else -> GroupReceiptTable.STATUS_UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
@@ -434,7 +434,7 @@ private fun rememberSecondaryAction(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewGeneric() {
|
||||
Previews.BottomSheetPreview {
|
||||
@@ -446,7 +446,7 @@ private fun BackupAlertSheetContentPreviewGeneric() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewPayment() {
|
||||
Previews.BottomSheetPreview {
|
||||
@@ -458,7 +458,7 @@ private fun BackupAlertSheetContentPreviewPayment() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewDelete() {
|
||||
Previews.BottomSheetPreview {
|
||||
@@ -473,7 +473,7 @@ private fun BackupAlertSheetContentPreviewDelete() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewDiskFull() {
|
||||
Previews.BottomSheetPreview {
|
||||
@@ -485,7 +485,7 @@ private fun BackupAlertSheetContentPreviewDiskFull() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewBackupFailed() {
|
||||
Previews.BottomSheetPreview {
|
||||
@@ -497,7 +497,7 @@ private fun BackupAlertSheetContentPreviewBackupFailed() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
|
||||
Previews.BottomSheetPreview {
|
||||
@@ -509,7 +509,7 @@ private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewSubscriptionExpired() {
|
||||
Previews.BottomSheetPreview {
|
||||
|
||||
@@ -31,8 +31,8 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
@@ -201,7 +201,7 @@ private fun BackupAlertSecondaryActionButton(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupAlertBottomSheetContainerPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
|
||||
@@ -14,8 +14,8 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.dimensionResource
|
||||
@@ -27,9 +27,10 @@ import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.setFragmentResult
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
@@ -52,15 +53,15 @@ class CreateBackupBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val isPaidTier: Boolean = remember { BackupStateObserver.getNonIOBackupState().isLikelyPaidTier() }
|
||||
|
||||
CreateBackupBottomSheetContent(
|
||||
isPaidTier = isPaidTier,
|
||||
onBackupNowClick = {
|
||||
BackupMessagesJob.enqueue()
|
||||
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to Result.BACKUP_STARTED))
|
||||
isResultSet = true
|
||||
dismissAllowingStateLoss()
|
||||
},
|
||||
onBackupLaterClick = {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -81,8 +82,8 @@ class CreateBackupBottomSheet : ComposeBottomSheetDialogFragment() {
|
||||
|
||||
@Composable
|
||||
private fun CreateBackupBottomSheetContent(
|
||||
onBackupNowClick: () -> Unit,
|
||||
onBackupLaterClick: () -> Unit
|
||||
isPaidTier: Boolean,
|
||||
onBackupNowClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
@@ -107,8 +108,14 @@ private fun CreateBackupBottomSheetContent(
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
val body = if (isPaidTier) {
|
||||
stringResource(id = R.string.CreateBackupBottomSheet__depending_on_the_size)
|
||||
} else {
|
||||
stringResource(id = R.string.CreateBackupBottomSheet__free_tier)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateBackupBottomSheet__depending_on_the_size),
|
||||
text = body,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
@@ -121,28 +128,30 @@ private fun CreateBackupBottomSheetContent(
|
||||
modifier = Modifier.widthIn(min = 220.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateBackupBottomSheet__back_up_now)
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onBackupLaterClick,
|
||||
modifier = Modifier.widthIn(min = 220.dp).padding(top = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.CreateBackupBottomSheet__back_up_later)
|
||||
text = stringResource(id = android.R.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun CreateBackupBottomSheetContentPreview() {
|
||||
private fun CreateBackupBottomSheetContentPaidPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
CreateBackupBottomSheetContent(
|
||||
onBackupNowClick = {},
|
||||
onBackupLaterClick = {}
|
||||
isPaidTier = true,
|
||||
onBackupNowClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun CreateBackupBottomSheetContentFreePreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
CreateBackupBottomSheetContent(
|
||||
isPaidTier = false,
|
||||
onBackupNowClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
|
||||
@@ -70,7 +70,7 @@ private fun DownloadYourBackupTodayDialogContent(
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun DownloadYourBackupTodayDialogContentPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -18,9 +18,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@@ -35,8 +33,8 @@ import androidx.core.os.BundleCompat
|
||||
import org.signal.core.ui.R
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.gibiBytes
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
@@ -158,7 +156,7 @@ private fun SheetContent(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BackupAlertSheetContentPreviewMedia() {
|
||||
Previews.BottomSheetPreview {
|
||||
|
||||
@@ -12,8 +12,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
||||
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
|
||||
@@ -71,7 +71,7 @@ private fun NoManualBackupSheetContent(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun NoManualBackupSheetContentPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
|
||||
@@ -19,8 +19,8 @@ import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository
|
||||
import org.thoughtcrime.securesms.components.contactsupport.ContactSupportDialogFragment
|
||||
@@ -108,7 +108,7 @@ private fun NoRemoteStorageSpaceAvailableBottomSheetContent(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun NoRemoteStorageSpaceAvailableBottomSheetContentPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
|
||||
@@ -29,8 +29,8 @@ import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
@@ -112,7 +112,7 @@ private fun BackupAlertText(text: AnnotatedString, inlineContent: Map<String, In
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusRowCouldNotCompleteBackupPreview() {
|
||||
Previews.Preview {
|
||||
@@ -120,7 +120,7 @@ fun BackupStatusRowCouldNotCompleteBackupPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusRowBackupFailedPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -35,8 +35,8 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.mebiBytes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
@@ -309,7 +309,7 @@ private fun ArchiveRestoreProgressState.actionResource(): Int {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusBannerPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -21,9 +21,9 @@ import androidx.compose.ui.res.dimensionResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Rows
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.mebiBytes
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.RestoreState
|
||||
@@ -98,7 +98,7 @@ fun BackupStatusRow(
|
||||
|
||||
if (backupStatusData.restoreStatus == RestoreStatus.NOT_ENOUGH_DISK_SPACE) {
|
||||
BackupAlertText(
|
||||
text = stringResource(R.string.BackupStatusRow__not_enough_space, backupStatusData.remainingRestoreSize)
|
||||
text = stringResource(R.string.BackupStatusRow__not_enough_space, backupStatusData.remainingRestoreSize.toUnitString())
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
@@ -221,7 +221,7 @@ private fun progressColor(backupStatusData: ArchiveRestoreProgressState): Color
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusRowNormalPreview() {
|
||||
Previews.Preview {
|
||||
@@ -232,7 +232,7 @@ fun BackupStatusRowNormalPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusRowWaitingForWifiPreview() {
|
||||
Previews.Preview {
|
||||
@@ -242,7 +242,7 @@ fun BackupStatusRowWaitingForWifiPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusRowWaitingForInternetPreview() {
|
||||
Previews.Preview {
|
||||
@@ -252,7 +252,7 @@ fun BackupStatusRowWaitingForInternetPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusRowLowBatteryPreview() {
|
||||
Previews.Preview {
|
||||
@@ -262,7 +262,7 @@ fun BackupStatusRowLowBatteryPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusRowFinishedPreview() {
|
||||
Previews.Preview {
|
||||
@@ -273,7 +273,7 @@ fun BackupStatusRowFinishedPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
fun BackupStatusRowNotEnoughFreeSpacePreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -43,9 +43,9 @@ import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.fonts.MonoTypeface
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.BackupKeyVisualTransformation
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.attachBackupKeyAutoFillHelper
|
||||
import org.thoughtcrime.securesms.registrationv3.ui.restore.backupKeyAutoFillHelper
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.BackupKeyVisualTransformation
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.attachBackupKeyAutoFillHelper
|
||||
import org.thoughtcrime.securesms.registration.ui.restore.backupKeyAutoFillHelper
|
||||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Represents the availability status of Google Play Services on the device.
|
||||
*
|
||||
* Maps Google Play Services ConnectionResult codes to enum values for easier handling
|
||||
* in the application. Each enum value corresponds to a specific state that determines
|
||||
* what dialog or action should be presented to the user.
|
||||
*
|
||||
* @param code The corresponding ConnectionResult code from Google Play Services
|
||||
*/
|
||||
enum class GooglePlayServicesAvailability(val code: Int) {
|
||||
/** An unknown code. Possibly due to an update on Google's end */
|
||||
UNKNOWN(code = Int.MIN_VALUE),
|
||||
|
||||
/** Google Play Services is available and ready to use */
|
||||
SUCCESS(code = ConnectionResult.SUCCESS),
|
||||
|
||||
/** Google Play Services is not installed on the device */
|
||||
SERVICE_MISSING(code = ConnectionResult.SERVICE_MISSING),
|
||||
|
||||
/** Google Play Services is currently being updated */
|
||||
SERVICE_UPDATING(code = ConnectionResult.SERVICE_UPDATING),
|
||||
|
||||
/** Google Play Services requires an update to a newer version */
|
||||
SERVICE_VERSION_UPDATE_REQUIRED(code = ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED),
|
||||
|
||||
/** Google Play Services is installed but disabled by the user */
|
||||
SERVICE_DISABLED(code = ConnectionResult.SERVICE_DISABLED),
|
||||
|
||||
/** Google Play Services installation is invalid or corrupted */
|
||||
SERVICE_INVALID(code = ConnectionResult.SERVICE_INVALID);
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = Log.tag(GooglePlayServicesAvailability::class)
|
||||
|
||||
/**
|
||||
* Converts a Google Play Services ConnectionResult code to the corresponding enum value.
|
||||
*
|
||||
* @param code The ConnectionResult code from Google Play Services
|
||||
* @return The matching GooglePlayServicesAvailability enum value
|
||||
*/
|
||||
fun fromCode(code: Int): GooglePlayServicesAvailability {
|
||||
val availability = entries.firstOrNull { it.code == code } ?: UNKNOWN
|
||||
if (availability == UNKNOWN) {
|
||||
Log.w(TAG, "Unknown availability code: $code")
|
||||
}
|
||||
|
||||
return availability
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog based on the Google Play Services availability status.
|
||||
*
|
||||
* Shows different dialogs with appropriate messages and actions depending on whether
|
||||
* Google Play Services is missing, updating, requires an update, is disabled, or invalid.
|
||||
* When availability is SUCCESS, automatically calls onDismissRequest to dismiss any dialog.
|
||||
*
|
||||
* @param onDismissRequest Callback invoked when the dialog is dismissed or when SUCCESS status is received
|
||||
* @param onLearnMoreClick Callback invoked when the "Learn More" action is selected
|
||||
* @param onMakeServicesAvailableClick Callback invoked when an action to make services
|
||||
* available is selected (e.g., install or update)
|
||||
* @param googlePlayServicesAvailability The current availability status of Google Play Services
|
||||
*/
|
||||
@Composable
|
||||
fun GooglePlayServicesAvailabilityDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onMakeServicesAvailableClick: () -> Unit,
|
||||
googlePlayServicesAvailability: GooglePlayServicesAvailability
|
||||
) {
|
||||
when (googlePlayServicesAvailability) {
|
||||
GooglePlayServicesAvailability.SUCCESS -> {
|
||||
LaunchedEffect(Unit) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_MISSING, GooglePlayServicesAvailability.UNKNOWN -> {
|
||||
ServiceMissingDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onInstallPlayServicesClick = onMakeServicesAvailableClick
|
||||
)
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_UPDATING -> {
|
||||
ServiceUpdatingDialog(onDismissRequest = onDismissRequest)
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_VERSION_UPDATE_REQUIRED -> {
|
||||
ServiceVersionUpdateRequiredDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onUpdateClick = onMakeServicesAvailableClick
|
||||
)
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_DISABLED -> {
|
||||
ServiceDisabledDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onLearnMoreClick = onLearnMoreClick
|
||||
)
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_INVALID -> {
|
||||
ServiceInvalidDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onLearnMoreClick = onLearnMoreClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceMissingDialog(onDismissRequest: () -> Unit, onInstallPlayServicesClick: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_missing_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_missing_message),
|
||||
confirm = stringResource(R.string.GooglePlayServicesAvailability__install_play_services),
|
||||
dismiss = stringResource(android.R.string.cancel),
|
||||
onConfirm = {},
|
||||
onDeny = onInstallPlayServicesClick,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceUpdatingDialog(onDismissRequest: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_updating_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_updating_message),
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
onConfirm = {},
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceVersionUpdateRequiredDialog(onDismissRequest: () -> Unit, onUpdateClick: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_update_required_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_update_required_message),
|
||||
confirm = stringResource(R.string.GooglePlayServicesAvailability__update),
|
||||
dismiss = stringResource(android.R.string.cancel),
|
||||
onConfirm = onUpdateClick,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceDisabledDialog(onDismissRequest: () -> Unit, onLearnMoreClick: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_message),
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
dismiss = stringResource(R.string.GooglePlayServicesAvailability__learn_more),
|
||||
onConfirm = onDismissRequest,
|
||||
onDeny = onLearnMoreClick,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceInvalidDialog(onDismissRequest: () -> Unit, onLearnMoreClick: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_invalid_message),
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
dismiss = stringResource(R.string.GooglePlayServicesAvailability__learn_more),
|
||||
onConfirm = {},
|
||||
onDeny = onLearnMoreClick,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun ServiceMissingDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceMissingDialog({}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun ServiceUpdatingDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceUpdatingDialog({})
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun ServiceVersionUpdateRequiredDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceVersionUpdateRequiredDialog({}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun ServiceDisabledDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceDisabledDialog({}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun ServiceInvalidDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceInvalidDialog({}, {})
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,9 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -144,7 +144,7 @@ fun MessageBackupsEducationScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MessageBackupsEducationSheetPreview() {
|
||||
Previews.Preview {
|
||||
@@ -156,7 +156,7 @@ private fun MessageBackupsEducationSheetPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun NotableFeatureRowPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlowable
|
||||
@@ -40,6 +41,7 @@ import org.thoughtcrime.securesms.compose.Nav
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
@@ -63,7 +65,10 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
}
|
||||
|
||||
private val viewModel: MessageBackupsFlowViewModel by viewModel {
|
||||
MessageBackupsFlowViewModel(requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java))
|
||||
MessageBackupsFlowViewModel(
|
||||
initialTierSelection = requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java),
|
||||
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext())
|
||||
)
|
||||
}
|
||||
|
||||
private val errorHandler = InAppPaymentCheckoutDelegate.ErrorHandler()
|
||||
@@ -97,6 +102,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshCurrentTier()
|
||||
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -170,7 +176,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
stage = state.stage,
|
||||
currentBackupTier = state.currentMessageBackupTier,
|
||||
selectedBackupTier = state.selectedMessageBackupTier,
|
||||
availableBackupTypes = state.availableBackupTypes,
|
||||
allBackupTypes = state.allBackupTypes,
|
||||
isNextEnabled = state.isCheckoutButtonEnabled(),
|
||||
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
|
||||
onNavigationClick = viewModel::goToPreviousStage,
|
||||
@@ -180,7 +186,23 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
getString(R.string.backup_support_url)
|
||||
)
|
||||
},
|
||||
onNextClicked = viewModel::goToNextStage
|
||||
onNextClicked = viewModel::goToNextStage,
|
||||
googlePlayServicesAvailability = state.googlePlayApiAvailability,
|
||||
googlePlayBillingAvailability = state.googlePlayBillingAvailability,
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade = {
|
||||
CommunicationActions.openBrowserLink(
|
||||
requireContext(),
|
||||
getString(R.string.backup_support_url)
|
||||
)
|
||||
},
|
||||
onMakeGooglePlayServicesAvailable = {
|
||||
GoogleApiAvailability.getInstance().makeGooglePlayServicesAvailable(requireActivity()).addOnSuccessListener {
|
||||
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
|
||||
}
|
||||
},
|
||||
onOpenPlayStore = {
|
||||
PlayStoreUtil.openPlayStoreHome(requireContext())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.signal.core.util.billing.BillingResponseCode
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
@@ -16,7 +17,9 @@ import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
data class MessageBackupsFlowState(
|
||||
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
|
||||
val currentMessageBackupTier: MessageBackupTier? = null,
|
||||
val availableBackupTypes: List<MessageBackupsType> = emptyList(),
|
||||
val allBackupTypes: List<MessageBackupsType> = emptyList(),
|
||||
val googlePlayApiAvailability: GooglePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
|
||||
val googlePlayBillingAvailability: BillingResponseCode = BillingResponseCode.FEATURE_NOT_SUPPORTED,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment? = null,
|
||||
val startScreen: MessageBackupsStage,
|
||||
val stage: MessageBackupsStage = startScreen,
|
||||
@@ -35,7 +38,7 @@ data class MessageBackupsFlowState(
|
||||
* Whether or not the 'next' button on the type selection screen is enabled.
|
||||
*/
|
||||
fun isCheckoutButtonEnabled(): Boolean {
|
||||
return selectedMessageBackupTier in availableBackupTypes.map { it.tier } &&
|
||||
return selectedMessageBackupTier in allBackupTypes.map { it.tier } &&
|
||||
selectedMessageBackupTier != currentMessageBackupTier &&
|
||||
paymentReadyState == PaymentReadyState.READY
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ import org.thoughtcrime.securesms.jobs.InAppPaymentPurchaseTokenJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.next
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
@@ -53,6 +52,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MessageBackupsFlowViewModel(
|
||||
private val initialTierSelection: MessageBackupTier?,
|
||||
googlePlayApiAvailability: Int,
|
||||
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
|
||||
) : ViewModel(), BackupKeyCredentialManagerHandler {
|
||||
|
||||
@@ -63,7 +63,8 @@ class MessageBackupsFlowViewModel(
|
||||
|
||||
private val internalStateFlow = MutableStateFlow(
|
||||
MessageBackupsFlowState(
|
||||
availableBackupTypes = emptyList(),
|
||||
allBackupTypes = emptyList(),
|
||||
googlePlayApiAvailability = GooglePlayServicesAvailability.fromCode(googlePlayApiAvailability),
|
||||
currentMessageBackupTier = SignalStore.backup.backupTier,
|
||||
selectedMessageBackupTier = resolveSelectedTier(initialTierSelection, SignalStore.backup.backupTier),
|
||||
startScreen = startScreen
|
||||
@@ -74,6 +75,14 @@ class MessageBackupsFlowViewModel(
|
||||
val deletionState: Flow<DeletionState> = SignalStore.backup.deletionStateFlow
|
||||
|
||||
init {
|
||||
viewModelScope.launch(SignalDispatchers.IO) {
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
googlePlayBillingAvailability = AppDependencies.billingApi.getApiAvailability()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = withContext(SignalDispatchers.IO) {
|
||||
BackupRepository.triggerBackupIdReservation()
|
||||
@@ -85,16 +94,16 @@ class MessageBackupsFlowViewModel(
|
||||
}
|
||||
|
||||
result.runOnStatusCodeError { code ->
|
||||
Log.d(TAG, "Failed to trigger backup id reservation. ($code)")
|
||||
Log.w(TAG, "Failed to trigger backup id reservation. ($code)")
|
||||
internalStateFlow.update { it.copy(paymentReadyState = MessageBackupsFlowState.PaymentReadyState.FAILED) }
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val availableBackupTypes: List<MessageBackupsType> = try {
|
||||
val allBackupTypes: List<MessageBackupsType> = try {
|
||||
withContext(SignalDispatchers.IO) {
|
||||
BackupRepository.getAvailableBackupsTypes(
|
||||
if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
|
||||
BackupRepository.getBackupTypes(
|
||||
listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -104,8 +113,8 @@ class MessageBackupsFlowViewModel(
|
||||
|
||||
internalStateFlow.update { state ->
|
||||
state.copy(
|
||||
availableBackupTypes = availableBackupTypes,
|
||||
selectedMessageBackupTier = if (state.selectedMessageBackupTier in availableBackupTypes.map { it.tier }) state.selectedMessageBackupTier else availableBackupTypes.firstOrNull()?.tier
|
||||
allBackupTypes = allBackupTypes,
|
||||
selectedMessageBackupTier = if (state.selectedMessageBackupTier in allBackupTypes.map { it.tier }) state.selectedMessageBackupTier else allBackupTypes.firstOrNull()?.tier
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -157,6 +166,12 @@ class MessageBackupsFlowViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setGooglePlayApiAvailability(googlePlayApiAvailability: Int) {
|
||||
internalStateFlow.update {
|
||||
it.copy(googlePlayApiAvailability = GooglePlayServicesAvailability.fromCode(googlePlayApiAvailability))
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshCurrentTier() {
|
||||
val tier = SignalStore.backup.backupTier
|
||||
if (tier == MessageBackupTier.PAID) {
|
||||
@@ -285,7 +300,7 @@ class MessageBackupsFlowViewModel(
|
||||
|
||||
MessageBackupTier.PAID -> {
|
||||
check(state.selectedMessageBackupTier == MessageBackupTier.PAID)
|
||||
check(state.availableBackupTypes.any { it.tier == state.selectedMessageBackupTier })
|
||||
check(state.allBackupTypes.any { it.tier == state.selectedMessageBackupTier })
|
||||
|
||||
viewModelScope.launch(SignalDispatchers.IO) {
|
||||
internalStateFlow.update { it.copy(inAppPayment = null) }
|
||||
|
||||
@@ -8,10 +8,13 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -25,9 +28,9 @@ import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
@@ -39,6 +42,8 @@ fun MessageBackupsKeyEducationScreen(
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onNextClick: () -> Unit = {}
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
|
||||
@@ -48,7 +53,8 @@ fun MessageBackupsKeyEducationScreen(
|
||||
modifier = Modifier
|
||||
.padding(it)
|
||||
.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
|
||||
.fillMaxSize(),
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Image(
|
||||
@@ -61,6 +67,7 @@ fun MessageBackupsKeyEducationScreen(
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.MessageBackupsKeyEducationScreen__your_backup_key),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
@@ -81,11 +88,16 @@ fun MessageBackupsKeyEducationScreen(
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(bottom = 24.dp)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp, bottom = 24.dp)
|
||||
) {
|
||||
Buttons.LargeTonal(
|
||||
onClick = onNextClick,
|
||||
@@ -100,7 +112,7 @@ fun MessageBackupsKeyEducationScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MessageBackupsKeyEducationScreenPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -47,10 +47,10 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.horizontalGutters
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
@@ -421,7 +421,7 @@ private suspend fun saveKeyToCredentialManager(
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MessageBackupsKeyRecordScreenPreview() {
|
||||
Previews.Preview {
|
||||
@@ -438,7 +438,7 @@ private fun MessageBackupsKeyRecordScreenPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun SaveKeyConfirmationDialogPreview() {
|
||||
Previews.Preview {
|
||||
@@ -452,7 +452,7 @@ private fun SaveKeyConfirmationDialogPreview() {
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun CreateNewBackupKeySheetContentPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
@@ -462,7 +462,7 @@ private fun CreateNewBackupKeySheetContentPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun DownloadMediaDialogPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -34,9 +34,9 @@ import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -192,7 +192,7 @@ private fun BottomSheetContent(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MessageBackupsKeyRecordScreenPreview() {
|
||||
Previews.Preview {
|
||||
@@ -202,7 +202,7 @@ private fun MessageBackupsKeyRecordScreenPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BottomSheetContentPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
|
||||
@@ -19,8 +19,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
@@ -58,7 +58,7 @@ fun MessageBackupsTypeFeatureRow(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MessageBackupsTypeFeatureRowPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -47,11 +48,12 @@ import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.billing.BillingResponseCode
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -74,12 +76,17 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
stage: MessageBackupsStage,
|
||||
currentBackupTier: MessageBackupTier?,
|
||||
selectedBackupTier: MessageBackupTier?,
|
||||
availableBackupTypes: List<MessageBackupsType>,
|
||||
allBackupTypes: List<MessageBackupsType>,
|
||||
googlePlayServicesAvailability: GooglePlayServicesAvailability,
|
||||
googlePlayBillingAvailability: BillingResponseCode,
|
||||
isNextEnabled: Boolean,
|
||||
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
|
||||
onNavigationClick: () -> Unit,
|
||||
onReadMoreClicked: () -> Unit,
|
||||
onNextClicked: () -> Unit
|
||||
onNextClicked: () -> Unit,
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade: () -> Unit,
|
||||
onMakeGooglePlayServicesAvailable: () -> Unit,
|
||||
onOpenPlayStore: () -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
@@ -144,7 +151,7 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
availableBackupTypes,
|
||||
allBackupTypes,
|
||||
{ _, item -> item.tier }
|
||||
) { index, item ->
|
||||
MessageBackupsTypeBlock(
|
||||
@@ -158,19 +165,40 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val hasCurrentBackupTier = currentBackupTier != null
|
||||
val paidTierNotAvailableDialogState = remember { PaidTierNotAvailableDialogState() }
|
||||
val onSubscribeButtonClick = remember(googlePlayServicesAvailability, googlePlayBillingAvailability, selectedBackupTier) {
|
||||
{
|
||||
if (selectedBackupTier == MessageBackupTier.PAID && googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS) {
|
||||
paidTierNotAvailableDialogState.displayGooglePlayApiErrorDialog = true
|
||||
} else if (selectedBackupTier == MessageBackupTier.PAID && !googlePlayBillingAvailability.isSuccess) {
|
||||
paidTierNotAvailableDialogState.displayGooglePlayBillingErrorDialog = true
|
||||
} else {
|
||||
onNextClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PaidTierNotAvailableDialogs(
|
||||
state = paidTierNotAvailableDialogState,
|
||||
onOpenPlayStore = onOpenPlayStore,
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade = onLearnMoreAboutWhyUserCanNotUpgrade,
|
||||
onMakeGooglePlayServicesAvailable = onMakeGooglePlayServicesAvailable,
|
||||
googlePlayServicesAvailability = googlePlayServicesAvailability
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onNextClicked,
|
||||
onClick = onSubscribeButtonClick,
|
||||
enabled = isNextEnabled,
|
||||
modifier = Modifier
|
||||
.testTag("subscribe-button")
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = if (hasCurrentBackupTier) 10.dp else 16.dp)
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
val text: String = if (currentBackupTier == null) {
|
||||
if (selectedBackupTier == MessageBackupTier.PAID && availableBackupTypes.map { it.tier }.contains(selectedBackupTier)) {
|
||||
val paidTier = availableBackupTypes.first { it.tier == MessageBackupTier.PAID } as MessageBackupsType.Paid
|
||||
if (selectedBackupTier == MessageBackupTier.PAID && (googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS || !googlePlayBillingAvailability.isSuccess)) {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__more_about_this_plan)
|
||||
} else if (selectedBackupTier == MessageBackupTier.PAID && allBackupTypes.map { it.tier }.contains(selectedBackupTier)) {
|
||||
val paidTier = allBackupTypes.first { it.tier == MessageBackupTier.PAID } as MessageBackupsType.Paid
|
||||
val context = LocalContext.current
|
||||
|
||||
val price = remember(paidTier) {
|
||||
@@ -183,6 +211,8 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
} else {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__subscribe)
|
||||
}
|
||||
} else if (selectedBackupTier == MessageBackupTier.PAID && (googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS || !googlePlayBillingAvailability.isSuccess)) {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__more_about_this_plan)
|
||||
} else {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__change_backup_type)
|
||||
}
|
||||
@@ -200,7 +230,54 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Stable
|
||||
class PaidTierNotAvailableDialogState {
|
||||
var displayGooglePlayBillingErrorDialog: Boolean by mutableStateOf(false)
|
||||
var displayGooglePlayApiErrorDialog: Boolean by mutableStateOf(false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaidTierNotAvailableDialogs(
|
||||
state: PaidTierNotAvailableDialogState,
|
||||
googlePlayServicesAvailability: GooglePlayServicesAvailability,
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade: () -> Unit,
|
||||
onMakeGooglePlayServicesAvailable: () -> Unit,
|
||||
onOpenPlayStore: () -> Unit
|
||||
) {
|
||||
if (state.displayGooglePlayApiErrorDialog) {
|
||||
GooglePlayServicesAvailabilityDialog(
|
||||
onDismissRequest = { state.displayGooglePlayApiErrorDialog = false },
|
||||
googlePlayServicesAvailability = googlePlayServicesAvailability,
|
||||
onLearnMoreClick = onLearnMoreAboutWhyUserCanNotUpgrade,
|
||||
onMakeServicesAvailableClick = onMakeGooglePlayServicesAvailable
|
||||
)
|
||||
}
|
||||
|
||||
if (state.displayGooglePlayBillingErrorDialog) {
|
||||
UserNotSignedInDialog(
|
||||
onDismissRequest = { state.displayGooglePlayBillingErrorDialog = false },
|
||||
onOpenPlayStore = onOpenPlayStore
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserNotSignedInDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onOpenPlayStore: () -> Unit
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
|
||||
body = "To subscribe to Signal Secure Backups, please sign into the Google Play store.",
|
||||
onConfirm = onOpenPlayStore,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirm = "Open Play Store",
|
||||
dismiss = stringResource(android.R.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MessageBackupsTypeSelectionScreenPreview() {
|
||||
var selectedBackupsType by remember { mutableStateOf(MessageBackupTier.FREE) }
|
||||
@@ -209,18 +286,23 @@ private fun MessageBackupsTypeSelectionScreenPreview() {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
stage = MessageBackupsStage.TYPE_SELECTION,
|
||||
selectedBackupTier = selectedBackupsType,
|
||||
availableBackupTypes = testBackupTypes(),
|
||||
allBackupTypes = testBackupTypes(),
|
||||
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
||||
onNavigationClick = {},
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = {},
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade = {},
|
||||
onMakeGooglePlayServicesAvailable = {},
|
||||
onOpenPlayStore = {},
|
||||
currentBackupTier = null,
|
||||
googlePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
|
||||
googlePlayBillingAvailability = BillingResponseCode.OK,
|
||||
isNextEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun MessageBackupsTypeSelectionScreenWithCurrentTierPreview() {
|
||||
var selectedBackupsType by remember { mutableStateOf(MessageBackupTier.FREE) }
|
||||
@@ -229,12 +311,17 @@ private fun MessageBackupsTypeSelectionScreenWithCurrentTierPreview() {
|
||||
MessageBackupsTypeSelectionScreen(
|
||||
stage = MessageBackupsStage.TYPE_SELECTION,
|
||||
selectedBackupTier = selectedBackupsType,
|
||||
availableBackupTypes = testBackupTypes(),
|
||||
allBackupTypes = testBackupTypes(),
|
||||
onMessageBackupsTierSelected = { selectedBackupsType = it },
|
||||
onNavigationClick = {},
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = {},
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade = {},
|
||||
onMakeGooglePlayServicesAvailable = {},
|
||||
onOpenPlayStore = {},
|
||||
currentBackupTier = MessageBackupTier.PAID,
|
||||
googlePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
|
||||
googlePlayBillingAvailability = BillingResponseCode.OK,
|
||||
isNextEnabled = true
|
||||
)
|
||||
}
|
||||
@@ -325,8 +412,12 @@ private fun getFormattedPricePerMonth(messageBackupsType: MessageBackupsType): S
|
||||
return when (messageBackupsType) {
|
||||
is MessageBackupsType.Free -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__free)
|
||||
is MessageBackupsType.Paid -> {
|
||||
val formattedAmount = FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
stringResource(id = R.string.MessageBackupsTypeSelectionScreen__s_month, formattedAmount)
|
||||
if (messageBackupsType.pricePerMonth.amount == BigDecimal.ZERO) {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__paid)
|
||||
} else {
|
||||
val formattedAmount = FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
stringResource(id = R.string.MessageBackupsTypeSelectionScreen__s_month, formattedAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BiometricDeviceAuthentication
|
||||
@@ -188,7 +188,7 @@ fun VerifyBackupPinScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun VerifyBackupKeyScreen() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.util
|
||||
|
||||
import okio.ByteString
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.whispersystems.signalservice.api.backup.MediaName
|
||||
|
||||
fun Frame.getAllReferencedArchiveAttachmentInfos(): Set<ArchiveAttachmentInfo> {
|
||||
val infos: MutableSet<ArchiveAttachmentInfo> = mutableSetOf()
|
||||
when {
|
||||
this.account != null -> infos += this.account.getAllReferencedArchiveAttachmentInfos()
|
||||
this.chat != null -> infos += this.chat.getAllReferencedArchiveAttachmentInfos()
|
||||
this.chatItem != null -> infos += this.chatItem.getAllReferencedArchiveAttachmentInfos()
|
||||
}
|
||||
return infos.toSet()
|
||||
}
|
||||
|
||||
private fun AccountData.getAllReferencedArchiveAttachmentInfos(): Set<ArchiveAttachmentInfo> {
|
||||
val info = this.accountSettings?.defaultChatStyle?.wallpaperPhoto?.toArchiveAttachmentInfo()
|
||||
|
||||
return if (info != null) {
|
||||
setOf(info)
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Chat.getAllReferencedArchiveAttachmentInfos(): Set<ArchiveAttachmentInfo> {
|
||||
val info = this.style?.wallpaperPhoto?.toArchiveAttachmentInfo()
|
||||
|
||||
return if (info != null) {
|
||||
setOf(info)
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ChatItem.getAllReferencedArchiveAttachmentInfos(): Set<ArchiveAttachmentInfo> {
|
||||
var out: MutableSet<ArchiveAttachmentInfo>? = null
|
||||
|
||||
// The user could have many chat items, and most will not have attachments. To avoid allocating unnecessary sets, we do this little trick.
|
||||
// (Note: emptySet() returns a constant under the hood, so that's fine)
|
||||
fun appendToOutput(item: ArchiveAttachmentInfo) {
|
||||
if (out == null) {
|
||||
out = mutableSetOf()
|
||||
}
|
||||
|
||||
out.add(item)
|
||||
}
|
||||
|
||||
this.contactMessage?.contact?.avatar?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
|
||||
this.directStoryReplyMessage?.textReply?.longText?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
|
||||
this.standardMessage?.attachments?.mapNotNull { it.pointer?.toArchiveAttachmentInfo() }?.forEach { appendToOutput(it) }
|
||||
this.standardMessage?.quote?.attachments?.mapNotNull { it.thumbnail?.pointer?.toArchiveAttachmentInfo(forQuote = true) }?.forEach { appendToOutput(it) }
|
||||
this.standardMessage?.linkPreview?.mapNotNull { it.image?.toArchiveAttachmentInfo() }?.forEach { appendToOutput(it) }
|
||||
this.standardMessage?.longText?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
|
||||
this.stickerMessage?.sticker?.data_?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
|
||||
this.viewOnceMessage?.attachment?.pointer?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
|
||||
|
||||
this.revisions.forEach { revision ->
|
||||
revision.getAllReferencedArchiveAttachmentInfos().forEach { appendToOutput(it) }
|
||||
}
|
||||
|
||||
return out ?: emptySet()
|
||||
}
|
||||
|
||||
private fun FilePointer.toArchiveAttachmentInfo(forQuote: Boolean = false): ArchiveAttachmentInfo? {
|
||||
if (this.locatorInfo?.key == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.locatorInfo.plaintextHash == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return ArchiveAttachmentInfo(
|
||||
plaintextHash = this.locatorInfo.plaintextHash,
|
||||
remoteKey = this.locatorInfo.key,
|
||||
cdn = this.locatorInfo.mediaTierCdnNumber ?: Cdn.CDN_0.cdnNumber,
|
||||
contentType = this.contentType,
|
||||
forQuote = forQuote
|
||||
)
|
||||
}
|
||||
|
||||
data class ArchiveAttachmentInfo(
|
||||
val plaintextHash: ByteString,
|
||||
val remoteKey: ByteString,
|
||||
val cdn: Int,
|
||||
val contentType: String?,
|
||||
val forQuote: Boolean
|
||||
) {
|
||||
val fullSizeMediaName: MediaName get() = MediaName.fromPlaintextHashAndRemoteKey(plaintextHash.toByteArray(), remoteKey.toByteArray())
|
||||
val thumbnailMediaName: MediaName get() = MediaName.fromPlaintextHashAndRemoteKeyForThumbnail(plaintextHash.toByteArray(), remoteKey.toByteArray())
|
||||
}
|
||||
@@ -12,9 +12,11 @@ import com.bumptech.glide.load.Key
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.badges.load.BadgeSpriteTransformation
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.serialization.UriSerializer
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -28,12 +30,13 @@ typealias OnBadgeClicked = (Badge, Boolean, Boolean) -> Unit
|
||||
*/
|
||||
@Stable
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class Badge(
|
||||
val id: String,
|
||||
val category: Category,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val imageUrl: Uri,
|
||||
@Serializable(with = UriSerializer::class) val imageUrl: Uri,
|
||||
val imageDensity: String,
|
||||
val expirationTimestamp: Long,
|
||||
val visible: Boolean,
|
||||
|
||||
@@ -12,8 +12,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -49,7 +49,7 @@ private fun Banner(contentPadding: PaddingValues, actionListener: (Boolean) -> U
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -11,8 +11,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -65,7 +65,7 @@ private fun Banner(contentPadding: PaddingValues, onLearnMoreClicked: () -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -11,8 +11,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -56,7 +56,7 @@ private fun Banner(contentPadding: PaddingValues, onLearnMoreClicked: () -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -12,8 +12,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -60,7 +60,7 @@ private fun Banner(contentPadding: PaddingValues, onUpdateClicked: () -> Unit =
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -67,7 +67,7 @@ private fun Banner(contentPadding: PaddingValues, onDismissListener: () -> Unit
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -12,8 +12,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -56,7 +56,7 @@ private fun Banner(contentPadding: PaddingValues, onUpdateNow: () -> Unit = {})
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -11,8 +11,8 @@ import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -62,7 +62,7 @@ private fun Banner(contentPadding: PaddingValues, suggestionsSize: Int, onAddMem
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreviewSingular() {
|
||||
Previews.Preview {
|
||||
@@ -70,7 +70,7 @@ private fun BannerPreviewSingular() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreviewPlural() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -87,7 +87,7 @@ private fun Banner(contentPadding: PaddingValues, daysUntilExpiry: Int, onUpdate
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreviewExpireToday() {
|
||||
Previews.Preview {
|
||||
@@ -98,7 +98,7 @@ private fun BannerPreviewExpireToday() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreviewExpireTomorrow() {
|
||||
Previews.Preview {
|
||||
@@ -109,7 +109,7 @@ private fun BannerPreviewExpireTomorrow() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreviewExpireLater() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -15,8 +15,8 @@ import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -66,7 +66,7 @@ private fun Banner(contentPadding: PaddingValues, suggestionsSize: Int, onViewCl
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreviewSingular() {
|
||||
Previews.Preview {
|
||||
@@ -74,7 +74,7 @@ private fun BannerPreviewSingular() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreviewPlural() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -12,8 +12,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
|
||||
@@ -41,7 +41,7 @@ private fun Banner(contentPadding: PaddingValues) {
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -59,7 +59,7 @@ private fun Banner(contentPadding: PaddingValues) {
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -11,8 +11,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
import org.thoughtcrime.securesms.banner.ui.compose.Action
|
||||
@@ -65,7 +65,7 @@ private fun Banner(contentPadding: PaddingValues, usernameSyncState: UsernameSyn
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreviewUsernameCorrupted() {
|
||||
Previews.Preview {
|
||||
@@ -73,7 +73,7 @@ private fun BannerPreviewUsernameCorrupted() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun BannerPreviewLinkCorrupted() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -34,8 +34,8 @@ import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
@@ -195,7 +195,7 @@ enum class Importance {
|
||||
}
|
||||
|
||||
@Composable
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
private fun BubblesOptOutPreview() {
|
||||
Previews.Preview {
|
||||
DefaultBanner(
|
||||
@@ -212,7 +212,7 @@ private fun BubblesOptOutPreview() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
private fun ForcedUpgradePreview() {
|
||||
Previews.Preview {
|
||||
DefaultBanner(
|
||||
@@ -228,7 +228,7 @@ private fun ForcedUpgradePreview() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
private fun FullyLoadedErrorPreview() {
|
||||
val actions = listOf(
|
||||
Action(R.string.ExpiredBuildReminder_update_now) { },
|
||||
|
||||
@@ -26,8 +26,8 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock
|
||||
@@ -145,7 +145,7 @@ private fun UpgradeToEnableOptimizedStorageSheetContent(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun UpgradeToEnableOptimizedStorageSheetContentPreview() {
|
||||
Previews.BottomSheetPreview {
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlowable
|
||||
@@ -59,6 +60,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
|
||||
private val viewModel: MessageBackupsFlowViewModel by viewModel {
|
||||
MessageBackupsFlowViewModel(
|
||||
initialTierSelection = MessageBackupTier.PAID,
|
||||
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
|
||||
startScreen = MessageBackupsStage.TYPE_SELECTION
|
||||
)
|
||||
}
|
||||
@@ -93,12 +95,18 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshCurrentTier()
|
||||
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val paidBackupType = state.availableBackupTypes.firstOrNull { it.tier == MessageBackupTier.PAID } as? MessageBackupsType.Paid
|
||||
val freeBackupType = state.availableBackupTypes.firstOrNull { it.tier == MessageBackupTier.FREE } as? MessageBackupsType.Free
|
||||
val paidBackupType = state.allBackupTypes.firstOrNull { it.tier == MessageBackupTier.PAID } as? MessageBackupsType.Paid
|
||||
val freeBackupType = state.allBackupTypes.firstOrNull { it.tier == MessageBackupTier.FREE } as? MessageBackupsType.Free
|
||||
|
||||
if (paidBackupType != null && freeBackupType != null) {
|
||||
UpgradeSheetContent(
|
||||
|
||||
@@ -27,8 +27,8 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.compose.BottomSheets
|
||||
import org.signal.core.ui.compose.Buttons
|
||||
import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock
|
||||
@@ -143,7 +143,7 @@ private fun UpgradeToStartMediaBackupSheetContent(
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun UpgradeToStartMediaBackupSheetContentPreview() {
|
||||
Previews.Preview {
|
||||
|
||||
@@ -24,6 +24,8 @@ import java.net.URLDecoder
|
||||
object CallLinks {
|
||||
private const val ROOT_KEY = "key"
|
||||
private const val EPOCH = "epoch"
|
||||
private const val LEGACY_HTTPS_LINK_PREFIX = "https://signal.link/call#key="
|
||||
private const val LEGACY_SGNL_LINK_PREFIX = "sgnl://signal.link/call#key="
|
||||
private const val HTTPS_LINK_PREFIX = "https://signal.link/call/#key="
|
||||
private const val SNGL_LINK_PREFIX = "sgnl://signal.link/call/#key="
|
||||
|
||||
@@ -60,9 +62,16 @@ object CallLinks {
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPrefixedCallLink(url: String): Boolean {
|
||||
return url.startsWith(HTTPS_LINK_PREFIX) ||
|
||||
url.startsWith(SNGL_LINK_PREFIX) ||
|
||||
url.startsWith(LEGACY_HTTPS_LINK_PREFIX) ||
|
||||
url.startsWith(LEGACY_SGNL_LINK_PREFIX)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isCallLink(url: String): Boolean {
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
if (!isPrefixedCallLink(url)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -76,7 +85,7 @@ object CallLinks {
|
||||
|
||||
@JvmStatic
|
||||
fun parseUrl(url: String): CallLinkParseResult? {
|
||||
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
|
||||
if (!isPrefixedCallLink(url)) {
|
||||
Log.w(TAG, "Invalid url prefix.")
|
||||
return null
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user