mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-15 22:43:19 +01:00
Compare commits
243 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f2496625e | ||
|
|
aaacd28d86 | ||
|
|
c24473e176 | ||
|
|
1311ec498f | ||
|
|
251cec5dee | ||
|
|
1e15a8c1d3 | ||
|
|
9c5c58794b | ||
|
|
50063854d7 | ||
|
|
79d2041e46 | ||
|
|
479b27ce94 | ||
|
|
a66857a7cc | ||
|
|
37815a3f39 | ||
|
|
b55ba67b66 | ||
|
|
37a2d5fbca | ||
|
|
d7b5c6bff3 | ||
|
|
f11028529e | ||
|
|
93ec322bb9 | ||
|
|
71e0468d2c | ||
|
|
816e3442a0 | ||
|
|
c37ed722dc | ||
|
|
e08c2966c3 | ||
|
|
976f80ff7e | ||
|
|
34a4bda331 | ||
|
|
a4077ccb4a | ||
|
|
21ada2a503 | ||
|
|
57a70c3085 | ||
|
|
16c8b88f0f | ||
|
|
b806952430 | ||
|
|
c0da0bd272 | ||
|
|
45239c2264 | ||
|
|
690236c4e5 | ||
|
|
ebee3f72e6 | ||
|
|
37cec7d44f | ||
|
|
187fd63a75 | ||
|
|
362cdfc463 | ||
|
|
863b443317 | ||
|
|
341c474610 | ||
|
|
cbb3c0911c | ||
|
|
890facc6f6 | ||
|
|
6fa8337058 | ||
|
|
3f1cb65e02 | ||
|
|
3551e7ec00 | ||
|
|
5ecf60a306 | ||
|
|
6659700a1c | ||
|
|
070174fee6 | ||
|
|
09003d85b1 | ||
|
|
ea87108def | ||
|
|
7a696f9a62 | ||
|
|
8ba57a2733 | ||
|
|
9824cc2cbe | ||
|
|
ad60cc72cb | ||
|
|
1950b80402 | ||
|
|
2acb47952b | ||
|
|
14cacaef86 | ||
|
|
958e815933 | ||
|
|
6b50be78c0 | ||
|
|
680223c4b6 | ||
|
|
1af914d5ef | ||
|
|
a2fc710261 | ||
|
|
10922594b3 | ||
|
|
abd80c5204 | ||
|
|
ff589e3b91 | ||
|
|
c80ccd70ec | ||
|
|
d22d18da47 | ||
|
|
75b41c34ea | ||
|
|
11557e4815 | ||
|
|
d698f74d0b | ||
|
|
ecbea9fd95 | ||
|
|
13f7a64139 | ||
|
|
39cb1c638e | ||
|
|
489b58ad67 | ||
|
|
f20fe33af9 | ||
|
|
6adddf4a0c | ||
|
|
16773c9b17 | ||
|
|
4b50365fa9 | ||
|
|
98766b9ebb | ||
|
|
45a739ce92 | ||
|
|
c0d7145ada | ||
|
|
f94c007af8 | ||
|
|
df19cb5795 | ||
|
|
e6ceb55092 | ||
|
|
bfe2b5cba9 | ||
|
|
571004df50 | ||
|
|
f32b59f0aa | ||
|
|
e4019d8595 | ||
|
|
0b66a8701e | ||
|
|
e62b8de1bc | ||
|
|
d5cd790871 | ||
|
|
664c22d8f1 | ||
|
|
143a61e312 | ||
|
|
baaad0e475 | ||
|
|
7086709082 | ||
|
|
7bd5ad8c0b | ||
|
|
df19c91ae2 | ||
|
|
e5872037e0 | ||
|
|
b782fabbb6 | ||
|
|
485b466bd2 | ||
|
|
3beac6dfa9 | ||
|
|
98290a9fa3 | ||
|
|
13dd59f226 | ||
|
|
d9c42a4135 | ||
|
|
644b93e5a3 | ||
|
|
3ff218f9c6 | ||
|
|
f572eb5322 | ||
|
|
d3eb480d31 | ||
|
|
ac52b5b992 | ||
|
|
5c181e774f | ||
|
|
05d25718da | ||
|
|
66c50bef44 | ||
|
|
26bd59c378 | ||
|
|
e90eae6080 | ||
|
|
86a7db7653 | ||
|
|
230de7e9dc | ||
|
|
4b8546a151 | ||
|
|
ecd214b91b | ||
|
|
6b5de6e3e5 | ||
|
|
58b6e49aae | ||
|
|
c480512600 | ||
|
|
3a5b6476aa | ||
|
|
cb171092cf | ||
|
|
71979b34db | ||
|
|
73142cea39 | ||
|
|
2ab2c6f039 | ||
|
|
0bea15c0af | ||
|
|
cbd78d78ba | ||
|
|
ac0604a753 | ||
|
|
0e57335be1 | ||
|
|
c4e64f6fa3 | ||
|
|
bf9716f206 | ||
|
|
057ffdbaaf | ||
|
|
65dc0d3f34 | ||
|
|
173ee95e62 | ||
|
|
789339afa7 | ||
|
|
21b518da7a | ||
|
|
57b6b8dcf1 | ||
|
|
543a85316e | ||
|
|
2fedb3a0ee | ||
|
|
ae450aed67 | ||
|
|
0abb4727fc | ||
|
|
4bc6eb96ff | ||
|
|
e6a126d416 | ||
|
|
fdf858f379 | ||
|
|
4151d123cd | ||
|
|
c8a9759eba | ||
|
|
c59b74627f | ||
|
|
f2191d2996 | ||
|
|
7dfffbd50b | ||
|
|
329fc52077 | ||
|
|
8976111f61 | ||
|
|
7402959ac6 | ||
|
|
220d3877a2 | ||
|
|
380c33642c | ||
|
|
7acb2bef3d | ||
|
|
1a103106a5 | ||
|
|
6025e423e8 | ||
|
|
54656ea14e | ||
|
|
fd00ed71b5 | ||
|
|
d4fba5f3c7 | ||
|
|
ce244f2e8f | ||
|
|
4ad466390f | ||
|
|
c5c9b09f7b | ||
|
|
0638b31c1f | ||
|
|
c3c713a75a | ||
|
|
9af1c72233 | ||
|
|
500a1e46ad | ||
|
|
4b446877af | ||
|
|
015548613a | ||
|
|
30b339a482 | ||
|
|
6dcb2e8d24 | ||
|
|
3e8e17526b | ||
|
|
30ecaf7aea | ||
|
|
f761008509 | ||
|
|
c3ab8dddd0 | ||
|
|
164f089d37 | ||
|
|
a021b400bd | ||
|
|
fac8f403be | ||
|
|
d85ab37828 | ||
|
|
1e35403c87 | ||
|
|
b99c2165fa | ||
|
|
303100bb6b | ||
|
|
b71ba79b8a | ||
|
|
54cd84b842 | ||
|
|
1565ecdcea | ||
|
|
0a99b68d25 | ||
|
|
f4fac5bd90 | ||
|
|
f6760b90da | ||
|
|
ad9b1f05b4 | ||
|
|
17581a7a5e | ||
|
|
b41bf66133 | ||
|
|
8bb3d71472 | ||
|
|
295d4b9466 | ||
|
|
5e490376f4 | ||
|
|
fa27531c00 | ||
|
|
2737e5613c | ||
|
|
d84612ebf4 | ||
|
|
96165ad5a8 | ||
|
|
19caef057e | ||
|
|
29cafb11eb | ||
|
|
7e458bfde0 | ||
|
|
2a3cb80217 | ||
|
|
3d382ee15e | ||
|
|
6069dfc6f8 | ||
|
|
dee19ed94a | ||
|
|
905b0681f5 | ||
|
|
b6a4e1f145 | ||
|
|
a0131bf39b | ||
|
|
7ed77a00df | ||
|
|
887c173d8f | ||
|
|
6362da7a50 | ||
|
|
1296365bed | ||
|
|
99ae7c5961 | ||
|
|
5c3ea712fe | ||
|
|
bc5cb454bf | ||
|
|
8a7c2c1e20 | ||
|
|
a81a675d59 | ||
|
|
1c66da7873 | ||
|
|
afe3cd1098 | ||
|
|
4f3ee9ca1d | ||
|
|
7771aaa501 | ||
|
|
5ad38c7960 | ||
|
|
0fb1514da2 | ||
|
|
f37efd7e15 | ||
|
|
1ae2464df1 | ||
|
|
0425b70d31 | ||
|
|
7b0d3f36dc | ||
|
|
14b917dc7e | ||
|
|
6184cc0307 | ||
|
|
870aa8e7b0 | ||
|
|
d88016669b | ||
|
|
a464b413d9 | ||
|
|
d719edf104 | ||
|
|
b36b00a11c | ||
|
|
a99db2b16e | ||
|
|
2744dec43a | ||
|
|
6f2cce1494 | ||
|
|
689ee243aa | ||
|
|
537fc0ef5c | ||
|
|
e647b31f29 | ||
|
|
b59932cd88 | ||
|
|
cfb4377de3 | ||
|
|
e861c022da | ||
|
|
59006d3182 | ||
|
|
503faea3a9 |
@@ -6,4 +6,15 @@ ij_kotlin_allow_trailing_comma_on_call_site = false
|
||||
ij_kotlin_allow_trailing_comma = false
|
||||
ktlint_code_style = intellij_idea
|
||||
twitter_compose_allowed_composition_locals=LocalExtendedColors
|
||||
ktlint_standard_class-naming = disabled
|
||||
ktlint_standard_class-naming = disabled
|
||||
|
||||
# below rules disabled during ktlint version migration because they were preexisting but should be corrected and re-enabled ASAP
|
||||
ktlint_function_naming_ignore_when_annotated_with = Composable
|
||||
ktlint_standard_property-naming = disabled
|
||||
ktlint_standard_enum-wrapping = disabled
|
||||
ktlint_standard_multiline-if-else = disabled
|
||||
ktlint_standard_backing-property-naming = disabled
|
||||
ktlint_standard_statement-wrapping = disabled
|
||||
internal:ktlint-suppression = disabled
|
||||
ktlint_standard_unnecessary-parentheses-before-trailing-lambda = disabled
|
||||
ktlint_standard_value-parameter-comment = disabled
|
||||
|
||||
@@ -21,17 +21,10 @@ plugins {
|
||||
|
||||
apply(from = "static-ips.gradle.kts")
|
||||
|
||||
val canonicalVersionCode = 1421
|
||||
val canonicalVersionName = "7.8.1"
|
||||
|
||||
val postFixSize = 100
|
||||
val abiPostFix: Map<String, Int> = mapOf(
|
||||
"universal" to 0,
|
||||
"armeabi-v7a" to 1,
|
||||
"arm64-v8a" to 2,
|
||||
"x86" to 3,
|
||||
"x86_64" to 4
|
||||
)
|
||||
val canonicalVersionCode = 1432
|
||||
val canonicalVersionName = "7.10.3"
|
||||
val currentHotfixVersion = 0
|
||||
val maxHotfixVersions = 100
|
||||
|
||||
val keystores: Map<String, Properties?> = mapOf("debug" to loadKeystoreProperties("keystore.debug.properties"))
|
||||
|
||||
@@ -79,7 +72,7 @@ wire {
|
||||
}
|
||||
|
||||
ktlint {
|
||||
version.set("0.49.1")
|
||||
version.set("1.2.1")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -92,6 +85,8 @@ android {
|
||||
useLibrary("org.apache.http.legacy")
|
||||
testBuildType = "instrumentation"
|
||||
|
||||
android.bundle.language.enableSplit = false
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = signalKotlinJvmTarget
|
||||
}
|
||||
@@ -152,11 +147,11 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.4"
|
||||
kotlinCompilerExtensionVersion = "1.5.4"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode = canonicalVersionCode * postFixSize
|
||||
versionCode = (canonicalVersionCode * maxHotfixVersions) + currentHotfixVersion
|
||||
versionName = canonicalVersionName
|
||||
|
||||
minSdk = signalMinSdkVersion
|
||||
@@ -208,6 +203,7 @@ android {
|
||||
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\"")
|
||||
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"")
|
||||
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.PRODUCTION")
|
||||
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.INFO")
|
||||
|
||||
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"")
|
||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"")
|
||||
@@ -386,6 +382,7 @@ android {
|
||||
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"")
|
||||
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"")
|
||||
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.STAGING")
|
||||
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.DEBUG")
|
||||
|
||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
|
||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
|
||||
@@ -405,7 +402,6 @@ android {
|
||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
if (output.baseName.contains("nightly")) {
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
|
||||
var tag = getCurrentGitTag()
|
||||
if (!tag.isNullOrEmpty()) {
|
||||
if (tag.startsWith("v")) {
|
||||
@@ -419,14 +415,9 @@ android {
|
||||
} else {
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
|
||||
|
||||
val abiName: String = output.getFilter("ABI") ?: "universal"
|
||||
val postFix: Int = abiPostFix[abiName]!!
|
||||
|
||||
if (postFix >= postFixSize) {
|
||||
throw AssertionError("postFix is too large")
|
||||
if (currentHotfixVersion >= maxHotfixVersions) {
|
||||
throw AssertionError("Hotfix version is too large!")
|
||||
}
|
||||
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -509,6 +500,7 @@ dependencies {
|
||||
implementation(libs.androidx.profileinstaller)
|
||||
implementation(libs.androidx.asynclayoutinflater)
|
||||
implementation(libs.androidx.asynclayoutinflater.appcompat)
|
||||
implementation(libs.androidx.emoji2)
|
||||
implementation(libs.firebase.messaging) {
|
||||
exclude(group = "com.google.firebase", module = "firebase-core")
|
||||
exclude(group = "com.google.firebase", module = "firebase-analytics")
|
||||
@@ -540,6 +532,7 @@ dependencies {
|
||||
}
|
||||
implementation(libs.stream)
|
||||
implementation(libs.lottie)
|
||||
implementation(libs.lottie.compose)
|
||||
implementation(libs.signal.android.database.sqlcipher)
|
||||
implementation(libs.androidx.sqlite)
|
||||
implementation(libs.google.ez.vcard) {
|
||||
@@ -551,6 +544,7 @@ dependencies {
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.kotlin.stdlib.jdk8)
|
||||
implementation(libs.kotlin.reflect)
|
||||
implementation(libs.kotlinx.coroutines.play.services)
|
||||
implementation(libs.jackson.module.kotlin)
|
||||
implementation(libs.rxjava3.rxandroid)
|
||||
implementation(libs.rxjava3.rxkotlin)
|
||||
@@ -712,7 +706,8 @@ fun Project.languageList(): List<String> {
|
||||
.map { valuesFolderName -> valuesFolderName.replace("values-", "") }
|
||||
.filter { valuesFolderName -> valuesFolderName != "values" }
|
||||
.map { languageCode -> languageCode.replace("-r", "_") }
|
||||
.distinct() + "en"
|
||||
.distinct()
|
||||
.sorted() + "en"
|
||||
}
|
||||
|
||||
fun String.capitalize(): String {
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
-keep class androidx.window.** { *; }
|
||||
|
||||
-keepclassmembers class * extends androidx.constraintlayout.motion.widget.Key {
|
||||
public <init>();
|
||||
}
|
||||
|
||||
# AGP generated dont warns
|
||||
-dontwarn com.android.org.conscrypt.SSLParametersImpl
|
||||
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
|
||||
|
||||
BIN
app/src/androidTest/assets/backupTests/account-data.binproto
Normal file
BIN
app/src/androidTest/assets/backupTests/account-data.binproto
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -5,7 +5,7 @@ import org.signal.core.util.logging.AndroidLogger
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
|
||||
import org.thoughtcrime.securesms.database.LogDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
|
||||
@@ -21,8 +21,8 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
|
||||
|
||||
override fun initializeAppDependencies() {
|
||||
val default = ApplicationDependencyProvider(this)
|
||||
ApplicationDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
|
||||
ApplicationDependencies.getDeadlockDetector().start()
|
||||
AppDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
|
||||
AppDependencies.deadlockDetector.start()
|
||||
}
|
||||
|
||||
override fun initializeLogging() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.SqlUtil
|
||||
@@ -24,27 +25,30 @@ import org.signal.core.util.toInt
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.Currency
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
@@ -79,18 +83,20 @@ class BackupTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
SignalStore.account().setE164(SELF_E164)
|
||||
SignalStore.account().setAci(SELF_ACI)
|
||||
SignalStore.account().setPni(SELF_PNI)
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
SignalStore.account.setE164(SELF_E164)
|
||||
SignalStore.account.setAci(SELF_ACI)
|
||||
SignalStore.account.setPni(SELF_PNI)
|
||||
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||
}
|
||||
|
||||
@Ignore("Will likely be removed soon")
|
||||
@Test
|
||||
fun emptyDatabase() {
|
||||
backupTest { }
|
||||
}
|
||||
|
||||
@Ignore("Will likely be removed soon")
|
||||
@Test
|
||||
fun noteToSelf() {
|
||||
backupTest {
|
||||
@@ -102,6 +108,7 @@ class BackupTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore("Will likely be removed soon")
|
||||
@Test
|
||||
fun individualChat() {
|
||||
backupTest {
|
||||
@@ -118,6 +125,7 @@ class BackupTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore("Will likely be removed soon")
|
||||
@Test
|
||||
fun individualRecipients() {
|
||||
backupTest {
|
||||
@@ -149,6 +157,7 @@ class BackupTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore("Will likely be removed soon")
|
||||
@Test
|
||||
fun individualCallLogs() {
|
||||
backupTest {
|
||||
@@ -231,9 +240,10 @@ class BackupTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Ignore("Will likely be removed soon")
|
||||
@Test
|
||||
fun accountData() {
|
||||
val context = ApplicationDependencies.getApplication()
|
||||
val context = AppDependencies.application
|
||||
|
||||
backupTest(validateKeyValue = true) {
|
||||
val self = Recipient.self()
|
||||
@@ -241,35 +251,34 @@ class BackupTest {
|
||||
// TODO note-to-self archived
|
||||
// TODO note-to-self unread
|
||||
|
||||
SignalStore.account().setAci(SELF_ACI)
|
||||
SignalStore.account().setPni(SELF_PNI)
|
||||
SignalStore.account().setE164(SELF_E164)
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
SignalStore.account.setAci(SELF_ACI)
|
||||
SignalStore.account.setPni(SELF_PNI)
|
||||
SignalStore.account.setE164(SELF_E164)
|
||||
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||
|
||||
SignalDatabase.recipients.setProfileKey(self.id, ProfileKey(Random.nextBytes(32)))
|
||||
SignalDatabase.recipients.setProfileName(self.id, ProfileName.fromParts("Peter", "Parker"))
|
||||
SignalDatabase.recipients.setProfileAvatar(self.id, "https://example.com/")
|
||||
|
||||
SignalStore.donationsValues().markUserManuallyCancelled()
|
||||
SignalStore.donationsValues().setSubscriber(Subscriber(SubscriberId.generate(), "USD"))
|
||||
SignalStore.donationsValues().setDisplayBadgesOnProfile(false)
|
||||
InAppPaymentsRepository.setSubscriber(InAppPaymentSubscriberRecord(SubscriberId.generate(), Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION, false, InAppPaymentData.PaymentMethodType.UNKNOWN))
|
||||
SignalStore.donations.setDisplayBadgesOnProfile(false)
|
||||
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
|
||||
SignalStore.phoneNumberPrivacy.phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
|
||||
SignalStore.phoneNumberPrivacy.phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
|
||||
|
||||
SignalStore.settings().isLinkPreviewsEnabled = false
|
||||
SignalStore.settings().isPreferSystemContactPhotos = true
|
||||
SignalStore.settings().universalExpireTimer = 42
|
||||
SignalStore.settings().setKeepMutedChatsArchived(true)
|
||||
SignalStore.settings.isLinkPreviewsEnabled = false
|
||||
SignalStore.settings.isPreferSystemContactPhotos = true
|
||||
SignalStore.settings.universalExpireTimer = 42
|
||||
SignalStore.settings.setKeepMutedChatsArchived(true)
|
||||
|
||||
SignalStore.storyValues().viewedReceiptsEnabled = false
|
||||
SignalStore.storyValues().userHasViewedOnboardingStory = true
|
||||
SignalStore.storyValues().isFeatureDisabled = false
|
||||
SignalStore.storyValues().userHasBeenNotifiedAboutStories = true
|
||||
SignalStore.storyValues().userHasSeenGroupStoryEducationSheet = true
|
||||
SignalStore.story.viewedReceiptsEnabled = false
|
||||
SignalStore.story.userHasViewedOnboardingStory = true
|
||||
SignalStore.story.isFeatureDisabled = false
|
||||
SignalStore.story.userHasBeenNotifiedAboutStories = true
|
||||
SignalStore.story.userHasSeenGroupStoryEducationSheet = true
|
||||
|
||||
SignalStore.emojiValues().reactions = listOf("a", "b", "c")
|
||||
SignalStore.emoji.reactions = listOf("a", "b", "c")
|
||||
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, false)
|
||||
TextSecurePreferences.setReadReceiptsEnabled(context, false)
|
||||
|
||||
@@ -5,11 +5,8 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import android.Manifest
|
||||
import android.app.UiAutomation
|
||||
import android.os.Environment
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import io.mockk.InternalPlatformDsl.toArray
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
@@ -17,6 +14,7 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TestName
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.test.getObjectDiff
|
||||
import org.signal.libsignal.messagebackup.MessageBackup
|
||||
import org.signal.libsignal.messagebackup.MessageBackupKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
@@ -29,9 +27,11 @@ import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Contact
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.DistributionListItem
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Group
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
||||
@@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
@@ -79,7 +80,17 @@ class ImportExportTest {
|
||||
|
||||
val defaultBackupInfo = BackupInfo(version = 1L, backupTimeMs = 123456L)
|
||||
val selfRecipient = Recipient(id = 1, self = Self())
|
||||
val releaseNotes = Recipient(id = 2, releaseNotes = ReleaseNotes())
|
||||
val myStory = Recipient(
|
||||
id = 2,
|
||||
distributionList = DistributionListItem(
|
||||
distributionId = DistributionId.MY_STORY.asUuid().toByteArray().toByteString(),
|
||||
distributionList = DistributionList(
|
||||
name = DistributionId.MY_STORY.toString(),
|
||||
privacyMode = DistributionList.PrivacyMode.ALL
|
||||
)
|
||||
)
|
||||
)
|
||||
val releaseNotes = Recipient(id = 3, releaseNotes = ReleaseNotes())
|
||||
val standardAccountData = AccountData(
|
||||
profileKey = SELF_PROFILE_KEY.serialize().toByteString(),
|
||||
username = "self.01",
|
||||
@@ -87,9 +98,11 @@ class ImportExportTest {
|
||||
givenName = "Peter",
|
||||
familyName = "Parker",
|
||||
avatarUrlPath = "https://example.com/",
|
||||
subscriberId = SubscriberId.generate().bytes.toByteString(),
|
||||
subscriberCurrencyCode = "USD",
|
||||
subscriptionManuallyCancelled = true,
|
||||
donationSubscriberData = AccountData.SubscriberData(
|
||||
subscriberId = SubscriberId.generate().bytes.toByteString(),
|
||||
currencyCode = "USD",
|
||||
manuallyCancelled = true
|
||||
),
|
||||
accountSettings = AccountData.AccountSettings(
|
||||
readReceipts = true,
|
||||
sealedSenderIndicators = true,
|
||||
@@ -111,16 +124,15 @@ class ImportExportTest {
|
||||
)
|
||||
)
|
||||
val alice = Recipient(
|
||||
id = 3,
|
||||
id = 4,
|
||||
contact = Contact(
|
||||
aci = TestRecipientUtils.nextAci().toByteString(),
|
||||
pni = TestRecipientUtils.nextPni().toByteString(),
|
||||
username = "cool.01",
|
||||
e164 = 141255501234,
|
||||
blocked = false,
|
||||
hidden = false,
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
visibility = Contact.Visibility.VISIBLE,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Alexa",
|
||||
@@ -130,23 +142,26 @@ class ImportExportTest {
|
||||
)
|
||||
|
||||
/**
|
||||
* When using standardFrames you must start recipient ids at 3.
|
||||
* When using standardFrames you must start recipient ids at 4.
|
||||
*/
|
||||
private val standardFrames = arrayOf(defaultBackupInfo, standardAccountData, selfRecipient, releaseNotes)
|
||||
private val standardFrames = arrayOf(defaultBackupInfo, standardAccountData, selfRecipient, myStory, releaseNotes)
|
||||
}
|
||||
|
||||
private val context: Context
|
||||
get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@JvmField
|
||||
@Rule
|
||||
var testName = TestName()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
SignalStore.svr().setMasterKey(MasterKey(MASTER_KEY), "1234")
|
||||
SignalStore.account().setE164(SELF_E164)
|
||||
SignalStore.account().setAci(SELF_ACI)
|
||||
SignalStore.account().setPni(SELF_PNI)
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234")
|
||||
SignalStore.account.setE164(SELF_E164)
|
||||
SignalStore.account.setAci(SELF_ACI)
|
||||
SignalStore.account.setPni(SELF_PNI)
|
||||
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -158,7 +173,7 @@ class ImportExportTest {
|
||||
fun largeNumberOfRecipientsAndChats() {
|
||||
val recipients = ArrayList<Recipient>(5000)
|
||||
val chats = ArrayList<Chat>(5000)
|
||||
var id = 3L
|
||||
var id = 4L
|
||||
for (i in 0..5000) {
|
||||
val recipientId = id++
|
||||
recipients.add(
|
||||
@@ -170,9 +185,8 @@ class ImportExportTest {
|
||||
username = "rec$i.01",
|
||||
e164 = 14125550000 + i,
|
||||
blocked = false,
|
||||
hidden = false,
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
visibility = Contact.Visibility.VISIBLE,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Test",
|
||||
@@ -216,7 +230,7 @@ class ImportExportTest {
|
||||
|
||||
@Test
|
||||
fun largeNumberOfMessagesAndChats() {
|
||||
val NUM_INDIVIDUAL_RECIPIENTS = 1000
|
||||
val numIndividualRecipients = 1000
|
||||
val numIndividualMessages = 500
|
||||
val numGroupMessagesPerPerson = 200
|
||||
|
||||
@@ -225,7 +239,7 @@ class ImportExportTest {
|
||||
val recipients = ArrayList<Recipient>(1010)
|
||||
val chats = ArrayList<Chat>(1010)
|
||||
var id = 3L
|
||||
for (i in 0 until NUM_INDIVIDUAL_RECIPIENTS) {
|
||||
for (i in 0 until numIndividualRecipients) {
|
||||
val recipientId = id++
|
||||
recipients.add(
|
||||
Recipient(
|
||||
@@ -236,9 +250,8 @@ class ImportExportTest {
|
||||
username = if (random.trueWithProbability(0.2f)) "rec$i.01" else null,
|
||||
e164 = 14125550000 + i,
|
||||
blocked = random.trueWithProbability(0.1f),
|
||||
hidden = random.trueWithProbability(0.1f),
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
visibility = if (random.trueWithProbability(0.1f)) Contact.Visibility.HIDDEN else Contact.Visibility.VISIBLE,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = random.trueWithProbability(0.9f),
|
||||
profileGivenName = "Test",
|
||||
@@ -369,12 +382,12 @@ class ImportExportTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
val import = exportFrames(
|
||||
|
||||
exportFrames(
|
||||
*standardFrames,
|
||||
*recipients.toArray(),
|
||||
*chatItems.toArray()
|
||||
)
|
||||
outputFile(import)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -382,16 +395,15 @@ class ImportExportTest {
|
||||
importExport(
|
||||
*standardFrames,
|
||||
Recipient(
|
||||
id = 3,
|
||||
id = 4,
|
||||
contact = Contact(
|
||||
aci = TestRecipientUtils.nextAci().toByteString(),
|
||||
pni = TestRecipientUtils.nextPni().toByteString(),
|
||||
username = "cool.01",
|
||||
e164 = 141255501234,
|
||||
blocked = true,
|
||||
hidden = true,
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
visibility = Contact.Visibility.VISIBLE,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Alexa",
|
||||
@@ -400,16 +412,15 @@ class ImportExportTest {
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 4,
|
||||
id = 5,
|
||||
contact = Contact(
|
||||
aci = null,
|
||||
pni = null,
|
||||
username = null,
|
||||
e164 = 141255501235,
|
||||
blocked = true,
|
||||
hidden = true,
|
||||
registered = Contact.Registered.NOT_REGISTERED,
|
||||
unregisteredTimestamp = 1234568927398L,
|
||||
visibility = Contact.Visibility.HIDDEN,
|
||||
notRegistered = Contact.NotRegistered(unregisteredTimestamp = 1234568927398L),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = false,
|
||||
profileGivenName = "Peter",
|
||||
@@ -425,7 +436,7 @@ class ImportExportTest {
|
||||
importExport(
|
||||
*standardFrames,
|
||||
Recipient(
|
||||
id = 3,
|
||||
id = 4,
|
||||
group = Group(
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = true,
|
||||
@@ -440,7 +451,7 @@ class ImportExportTest {
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 4,
|
||||
id = 5,
|
||||
group = Group(
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = false,
|
||||
@@ -462,16 +473,15 @@ class ImportExportTest {
|
||||
importExport(
|
||||
*standardFrames,
|
||||
Recipient(
|
||||
id = 3,
|
||||
id = 4,
|
||||
contact = Contact(
|
||||
aci = TestRecipientUtils.nextAci().toByteString(),
|
||||
pni = TestRecipientUtils.nextPni().toByteString(),
|
||||
username = "cool.01",
|
||||
e164 = 141255501234,
|
||||
blocked = true,
|
||||
hidden = true,
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
visibility = Contact.Visibility.HIDDEN,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Alexa",
|
||||
@@ -479,35 +489,33 @@ class ImportExportTest {
|
||||
hideStory = true
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 4,
|
||||
contact = Contact(
|
||||
aci = null,
|
||||
pni = null,
|
||||
username = null,
|
||||
e164 = 141255501235,
|
||||
blocked = true,
|
||||
hidden = true,
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Peter",
|
||||
profileFamilyName = "Kim",
|
||||
hideStory = true
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 5,
|
||||
contact = Contact(
|
||||
aci = null,
|
||||
pni = null,
|
||||
username = null,
|
||||
e164 = 141255501235,
|
||||
blocked = true,
|
||||
visibility = Contact.Visibility.HIDDEN,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Peter",
|
||||
profileFamilyName = "Kim",
|
||||
hideStory = true
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 6,
|
||||
contact = Contact(
|
||||
aci = null,
|
||||
pni = null,
|
||||
username = null,
|
||||
e164 = 141255501236,
|
||||
blocked = true,
|
||||
hidden = true,
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
visibility = Contact.Visibility.HIDDEN,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Father",
|
||||
@@ -516,14 +524,15 @@ class ImportExportTest {
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 6,
|
||||
distributionList = DistributionList(
|
||||
name = "Kim Family",
|
||||
id = 7,
|
||||
distributionList = DistributionListItem(
|
||||
distributionId = DistributionId.create().asUuid().toByteArray().toByteString(),
|
||||
allowReplies = true,
|
||||
deletionTimestamp = 0L,
|
||||
privacyMode = DistributionList.PrivacyMode.ONLY_WITH,
|
||||
memberRecipientIds = listOf(3, 4, 5)
|
||||
distributionList = DistributionList(
|
||||
name = "Kim Family",
|
||||
allowReplies = true,
|
||||
privacyMode = DistributionList.PrivacyMode.ONLY_WITH,
|
||||
memberRecipientIds = listOf(3, 4, 5)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -532,16 +541,15 @@ class ImportExportTest {
|
||||
@Test
|
||||
fun deletedDistributionList() {
|
||||
val alexa = Recipient(
|
||||
id = 3,
|
||||
id = 4,
|
||||
contact = Contact(
|
||||
aci = TestRecipientUtils.nextAci().toByteString(),
|
||||
pni = TestRecipientUtils.nextPni().toByteString(),
|
||||
username = "cool.01",
|
||||
e164 = 141255501234,
|
||||
blocked = true,
|
||||
hidden = true,
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
visibility = Contact.Visibility.HIDDEN,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Alexa",
|
||||
@@ -554,23 +562,19 @@ class ImportExportTest {
|
||||
alexa,
|
||||
Recipient(
|
||||
id = 6,
|
||||
distributionList = DistributionList(
|
||||
name = "Deleted list",
|
||||
distributionList = DistributionListItem(
|
||||
distributionId = DistributionId.create().asUuid().toByteArray().toByteString(),
|
||||
allowReplies = true,
|
||||
deletionTimestamp = 12345L,
|
||||
privacyMode = DistributionList.PrivacyMode.ONLY_WITH,
|
||||
memberRecipientIds = listOf(3)
|
||||
deletionTimestamp = 12345L
|
||||
)
|
||||
)
|
||||
)
|
||||
import(importData)
|
||||
val exported = export()
|
||||
val exported = BackupRepository.export()
|
||||
val expected = exportFrames(
|
||||
*standardFrames,
|
||||
alexa
|
||||
)
|
||||
outputFile(importData, expected)
|
||||
|
||||
compare(expected, exported)
|
||||
}
|
||||
|
||||
@@ -579,16 +583,15 @@ class ImportExportTest {
|
||||
importExport(
|
||||
*standardFrames,
|
||||
Recipient(
|
||||
id = 3,
|
||||
id = 4,
|
||||
contact = Contact(
|
||||
aci = TestRecipientUtils.nextAci().toByteString(),
|
||||
pni = TestRecipientUtils.nextPni().toByteString(),
|
||||
username = "cool.01",
|
||||
e164 = 141255501234,
|
||||
blocked = false,
|
||||
hidden = false,
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
visibility = Contact.Visibility.VISIBLE,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Alexa",
|
||||
@@ -597,7 +600,7 @@ class ImportExportTest {
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 4,
|
||||
id = 5,
|
||||
group = Group(
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = true,
|
||||
@@ -607,14 +610,13 @@ class ImportExportTest {
|
||||
),
|
||||
Chat(
|
||||
id = 1,
|
||||
recipientId = 3,
|
||||
recipientId = 4,
|
||||
archived = true,
|
||||
pinnedOrder = 1,
|
||||
expirationTimerMs = 1.days.inWholeMilliseconds,
|
||||
muteUntilMs = System.currentTimeMillis(),
|
||||
markedUnread = true,
|
||||
dontNotifyForMentionsIfMuted = true,
|
||||
wallpaper = null
|
||||
dontNotifyForMentionsIfMuted = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -681,16 +683,15 @@ class ImportExportTest {
|
||||
importExport(
|
||||
*standardFrames,
|
||||
Recipient(
|
||||
id = 3,
|
||||
id = 4,
|
||||
contact = Contact(
|
||||
aci = startedAci,
|
||||
pni = TestRecipientUtils.nextPni().toByteString(),
|
||||
username = "cool.01",
|
||||
e164 = 141255501234,
|
||||
blocked = false,
|
||||
hidden = false,
|
||||
registered = Contact.Registered.REGISTERED,
|
||||
unregisteredTimestamp = 0L,
|
||||
visibility = Contact.Visibility.VISIBLE,
|
||||
registered = Contact.Registered(),
|
||||
profileKey = TestRecipientUtils.generateProfileKey().toByteString(),
|
||||
profileSharing = true,
|
||||
profileGivenName = "Alexa",
|
||||
@@ -699,7 +700,7 @@ class ImportExportTest {
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 4,
|
||||
id = 5,
|
||||
group = Group(
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = true,
|
||||
@@ -709,14 +710,13 @@ class ImportExportTest {
|
||||
),
|
||||
Chat(
|
||||
id = 1,
|
||||
recipientId = 3,
|
||||
recipientId = 4,
|
||||
archived = true,
|
||||
pinnedOrder = 1,
|
||||
expirationTimerMs = 1.days.inWholeMilliseconds,
|
||||
muteUntilMs = System.currentTimeMillis(),
|
||||
markedUnread = true,
|
||||
dontNotifyForMentionsIfMuted = true,
|
||||
wallpaper = null
|
||||
dontNotifyForMentionsIfMuted = true
|
||||
),
|
||||
*individualCalls.toArray()
|
||||
)
|
||||
@@ -994,14 +994,13 @@ class ImportExportTest {
|
||||
expirationNotStarted
|
||||
)
|
||||
import(importData)
|
||||
val exported = export()
|
||||
val exported = BackupRepository.export()
|
||||
val expected = exportFrames(
|
||||
*standardFrames,
|
||||
alice,
|
||||
chat,
|
||||
expirationNotStarted
|
||||
)
|
||||
outputFile(importData, expected)
|
||||
compare(expected, exported)
|
||||
}
|
||||
|
||||
@@ -1051,23 +1050,6 @@ class ImportExportTest {
|
||||
incrementalMacChunkSize = 0
|
||||
),
|
||||
wasDownloaded = false
|
||||
),
|
||||
MessageAttachment(
|
||||
pointer = FilePointer(
|
||||
backupLocator = FilePointer.BackupLocator(
|
||||
"digestherebutimlazy",
|
||||
cdnNumber = 3,
|
||||
key = (1..32).map { it.toByte() }.toByteArray().toByteString(),
|
||||
digest = (1..64).map { it.toByte() }.toByteArray().toByteString(),
|
||||
size = 12345
|
||||
),
|
||||
contentType = "image/png",
|
||||
width = 100,
|
||||
height = 200,
|
||||
caption = "Love this cool picture! Too bad u cant download it",
|
||||
incrementalMacChunkSize = 0
|
||||
),
|
||||
wasDownloaded = true
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1256,6 +1238,76 @@ class ImportExportTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun giftBadgeMessage() {
|
||||
var dateSentStart = 100L
|
||||
importExport(
|
||||
*standardFrames,
|
||||
alice,
|
||||
buildChat(alice, 1),
|
||||
ChatItem(
|
||||
chatId = 1,
|
||||
authorId = alice.id,
|
||||
dateSent = dateSentStart++,
|
||||
incoming = ChatItem.IncomingMessageDetails(
|
||||
dateReceived = dateSentStart,
|
||||
dateServerSent = dateSentStart,
|
||||
read = true,
|
||||
sealedSender = true
|
||||
),
|
||||
giftBadge = GiftBadge(
|
||||
receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(),
|
||||
state = GiftBadge.State.OPENED
|
||||
)
|
||||
),
|
||||
ChatItem(
|
||||
chatId = 1,
|
||||
authorId = alice.id,
|
||||
dateSent = dateSentStart++,
|
||||
incoming = ChatItem.IncomingMessageDetails(
|
||||
dateReceived = dateSentStart,
|
||||
dateServerSent = dateSentStart,
|
||||
read = true,
|
||||
sealedSender = true
|
||||
),
|
||||
giftBadge = GiftBadge(
|
||||
receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(),
|
||||
state = GiftBadge.State.FAILED
|
||||
)
|
||||
),
|
||||
ChatItem(
|
||||
chatId = 1,
|
||||
authorId = alice.id,
|
||||
dateSent = dateSentStart++,
|
||||
incoming = ChatItem.IncomingMessageDetails(
|
||||
dateReceived = dateSentStart,
|
||||
dateServerSent = dateSentStart,
|
||||
read = true,
|
||||
sealedSender = true
|
||||
),
|
||||
giftBadge = GiftBadge(
|
||||
receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(),
|
||||
state = GiftBadge.State.REDEEMED
|
||||
)
|
||||
),
|
||||
ChatItem(
|
||||
chatId = 1,
|
||||
authorId = alice.id,
|
||||
dateSent = dateSentStart++,
|
||||
incoming = ChatItem.IncomingMessageDetails(
|
||||
dateReceived = dateSentStart,
|
||||
dateServerSent = dateSentStart,
|
||||
read = true,
|
||||
sealedSender = true
|
||||
),
|
||||
giftBadge = GiftBadge(
|
||||
receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(),
|
||||
state = GiftBadge.State.UNOPENED
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun enumerateIncomingMessageDetails(dateSent: Long): List<ChatItem.IncomingMessageDetails> {
|
||||
val details = mutableListOf<ChatItem.IncomingMessageDetails>()
|
||||
details.add(
|
||||
@@ -1361,8 +1413,7 @@ class ImportExportTest {
|
||||
expirationTimerMs = 0,
|
||||
muteUntilMs = 0,
|
||||
markedUnread = false,
|
||||
dontNotifyForMentionsIfMuted = false,
|
||||
wallpaper = null
|
||||
dontNotifyForMentionsIfMuted = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1373,8 +1424,8 @@ class ImportExportTest {
|
||||
private fun exportFrames(vararg objects: Any): ByteArray {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val writer = EncryptedBackupWriter(
|
||||
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
|
||||
aci = SignalStore.account().aci!!,
|
||||
key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
|
||||
aci = SignalStore.account.aci!!,
|
||||
outputStream = outputStream,
|
||||
append = { mac -> outputStream.write(mac) }
|
||||
)
|
||||
@@ -1396,46 +1447,31 @@ class ImportExportTest {
|
||||
return outputStream.toByteArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the passed in frames as a backup and then attempts to
|
||||
* import them.
|
||||
*/
|
||||
private fun import(vararg objects: Any) {
|
||||
val importData = exportFrames(*objects)
|
||||
import(importData)
|
||||
}
|
||||
|
||||
private fun import(importData: ByteArray) {
|
||||
BackupRepository.import(length = importData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(importData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
|
||||
}
|
||||
|
||||
/**
|
||||
* Export our current database as a backup.
|
||||
*/
|
||||
private fun export(): ByteArray {
|
||||
val exportData = BackupRepository.export()
|
||||
return exportData
|
||||
}
|
||||
|
||||
private fun validate(importData: ByteArray): MessageBackup.ValidationResult {
|
||||
val factory = { ByteArrayInputStream(importData) }
|
||||
val masterKey = SignalStore.svr().getOrCreateMasterKey()
|
||||
val masterKey = SignalStore.svr.getOrCreateMasterKey()
|
||||
val key = MessageBackupKey(masterKey.serialize(), org.signal.libsignal.protocol.ServiceId.Aci.parseFromBinary(SELF_ACI.toByteArray()))
|
||||
|
||||
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, factory, importData.size.toLong())
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports the passed in frames and then exports them.
|
||||
* Given some [Frame]s, this will do the following:
|
||||
*
|
||||
* It will do a comparison to assert that the import and export
|
||||
* are equal.
|
||||
* 1. Write the frames using an [EncryptedBackupWriter] and keep the result in memory (A).
|
||||
* 2. Import those frames back into the local database.
|
||||
* 3. Export the state of the local database and keep the result in memory (B).
|
||||
* 4. Assert that (A) and (B) are identical. Or, in other words, assert that importing and exporting again results in the original backup data.
|
||||
*/
|
||||
private fun importExport(vararg objects: Any) {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
val writer = EncryptedBackupWriter(
|
||||
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
|
||||
aci = SignalStore.account().aci!!,
|
||||
key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
|
||||
aci = SignalStore.account.aci!!,
|
||||
outputStream = outputStream,
|
||||
append = { mac -> outputStream.write(mac) }
|
||||
)
|
||||
@@ -1454,12 +1490,13 @@ class ImportExportTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
val importData = outputStream.toByteArray()
|
||||
outputFile(importData)
|
||||
BackupRepository.import(length = importData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(importData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
|
||||
|
||||
val export = export()
|
||||
compare(importData, export)
|
||||
val originalBackupData = outputStream.toByteArray()
|
||||
|
||||
BackupRepository.import(length = originalBackupData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(originalBackupData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
|
||||
|
||||
val generatedBackupData = BackupRepository.export()
|
||||
compare(originalBackupData, generatedBackupData)
|
||||
}
|
||||
|
||||
private fun compare(import: ByteArray, export: ByteArray) {
|
||||
@@ -1498,7 +1535,14 @@ class ImportExportTest {
|
||||
for (f in framesExported) {
|
||||
when {
|
||||
f.account != null -> accountImported.add(f.account!!)
|
||||
f.recipient != null -> recipientsExported.add(f.recipient!!)
|
||||
f.recipient != null -> {
|
||||
val frameRecipient = f.recipient!!
|
||||
if (frameRecipient.distributionList != null && frameRecipient.distributionList!!.distributionId == DistributionId.MY_STORY.asUuid().toByteArray().toByteString()) {
|
||||
recipientsExported.add(frameRecipient.copy(distributionList = frameRecipient.distributionList!!.copyWithoutMembers()))
|
||||
} else {
|
||||
recipientsExported.add(f.recipient!!)
|
||||
}
|
||||
}
|
||||
f.chat != null -> chatsExported.add(f.chat!!)
|
||||
f.chatItem != null -> chatItemsExported.add(f.chatItem!!)
|
||||
f.adHocCall != null -> callsExported.add(f.adHocCall!!)
|
||||
@@ -1513,11 +1557,19 @@ class ImportExportTest {
|
||||
prettyAssertEquals(stickersImported, stickersExported) { it.packId }
|
||||
}
|
||||
|
||||
private fun <T> prettyAssertEquals(import: List<T>, export: List<T>) {
|
||||
private fun DistributionListItem.copyWithoutMembers(): DistributionListItem {
|
||||
return this.copy(
|
||||
distributionList = this.distributionList?.copy(
|
||||
memberRecipientIds = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <reified T : Any> prettyAssertEquals(import: List<T>, export: List<T>) {
|
||||
Assert.assertEquals(import.size, export.size)
|
||||
import.zip(export).forEach { (a1, a2) ->
|
||||
if (a1 != a2) {
|
||||
Assert.fail("Items do not match: \n $a1 \n $a2")
|
||||
Assert.fail("Items do not match:\n\n-- Pretty diff\n${getObjectDiff(a1, a2)}\n-- Full objects\n$a1\n$a2")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1526,19 +1578,31 @@ class ImportExportTest {
|
||||
return nextFloat() < prob
|
||||
}
|
||||
|
||||
private fun <T, R : Comparable<R>> prettyAssertEquals(import: List<T>, export: List<T>, selector: (T) -> R?) {
|
||||
private inline fun <reified T : Any, R : Comparable<R>> prettyAssertEquals(import: List<T>, export: List<T>, crossinline selector: (T) -> R?) {
|
||||
if (import.size != export.size) {
|
||||
var msg = StringBuilder()
|
||||
val msg = StringBuilder()
|
||||
msg.append("There's a different number of items in the lists!\n\n")
|
||||
|
||||
msg.append("Imported:\n")
|
||||
for (i in import) {
|
||||
msg.append(i)
|
||||
msg.append("\n")
|
||||
}
|
||||
if (import.isEmpty()) {
|
||||
msg.append("<None>")
|
||||
}
|
||||
msg.append("\n")
|
||||
msg.append("Exported:\n")
|
||||
for (i in export) {
|
||||
msg.append(i)
|
||||
msg.append("\n")
|
||||
}
|
||||
if (export.isEmpty()) {
|
||||
msg.append("<None>")
|
||||
}
|
||||
Assert.fail(msg.toString())
|
||||
}
|
||||
|
||||
Assert.assertEquals(import.size, export.size)
|
||||
val sortedImport = import.sortedBy(selector)
|
||||
val sortedExport = export.sortedBy(selector)
|
||||
@@ -1549,9 +1613,9 @@ class ImportExportTest {
|
||||
private fun readAllFrames(import: ByteArray, selfData: BackupRepository.SelfData): List<Frame> {
|
||||
val inputFactory = { ByteArrayInputStream(import) }
|
||||
val frameReader = EncryptedBackupReader(
|
||||
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
|
||||
key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
|
||||
aci = selfData.aci,
|
||||
streamLength = import.size.toLong(),
|
||||
length = import.size.toLong(),
|
||||
dataStream = inputFactory
|
||||
)
|
||||
val frames = ArrayList<Frame>()
|
||||
@@ -1562,9 +1626,8 @@ class ImportExportTest {
|
||||
return frames
|
||||
}
|
||||
|
||||
private fun outputFile(importBytes: ByteArray, resultBytes: ByteArray? = null) {
|
||||
grantPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
val dir = File(Environment.getExternalStorageDirectory(), "backup-tests")
|
||||
private fun writeToOutputFile(importBytes: ByteArray, resultBytes: ByteArray? = null) {
|
||||
val dir = File(context.filesDir, "backup-tests")
|
||||
if (dir.mkdirs() || dir.exists()) {
|
||||
FileOutputStream(File(dir, testName.methodName + ".import")).use {
|
||||
it.write(importBytes)
|
||||
@@ -1579,11 +1642,4 @@ class ImportExportTest {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun grantPermissions(vararg permissions: String?) {
|
||||
val auto: UiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation
|
||||
for (perm in permissions) {
|
||||
auto.grantRuntimePermissionAsUser(InstrumentationRegistry.getInstrumentation().targetContext.packageName, perm, android.os.Process.myUserHandle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class ImportExportTestSuite(private val path: String) {
|
||||
companion object {
|
||||
val SELF_ACI = ServiceId.ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
|
||||
val SELF_PNI = ServiceId.PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
|
||||
const val SELF_E164 = "+10000000000"
|
||||
val SELF_PROFILE_KEY = ProfileKey(Random.nextBytes(32))
|
||||
val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do")
|
||||
|
||||
const val TESTS_FOLDER = "backupTests"
|
||||
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun data(): Collection<Array<String>> {
|
||||
val testFiles = InstrumentationRegistry.getInstrumentation().context.resources.assets.list(TESTS_FOLDER)
|
||||
return testFiles?.map { arrayOf(it) }!!.toList()
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234")
|
||||
SignalStore.account.setE164(SELF_E164)
|
||||
SignalStore.account.setAci(SELF_ACI)
|
||||
SignalStore.account.setPni(SELF_PNI)
|
||||
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBinProto() {
|
||||
val binProtoBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("${TESTS_FOLDER}/$path").use {
|
||||
StreamUtil.readFully(it)
|
||||
}
|
||||
import(binProtoBytes)
|
||||
val generatedBackupData = BackupRepository.export()
|
||||
compare(binProtoBytes, generatedBackupData)
|
||||
}
|
||||
|
||||
private fun import(importData: ByteArray) {
|
||||
BackupRepository.import(
|
||||
length = importData.size.toLong(),
|
||||
inputStreamFactory = { ByteArrayInputStream(importData) },
|
||||
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY),
|
||||
plaintext = true
|
||||
)
|
||||
}
|
||||
|
||||
// TODO compare with libsignal's library
|
||||
private fun compare(import: ByteArray, export: ByteArray) {
|
||||
}
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.changenumber
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.FlakyTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.MockProvider
|
||||
import org.thoughtcrime.securesms.testing.Post
|
||||
import org.thoughtcrime.securesms.testing.Put
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsSize
|
||||
import org.thoughtcrime.securesms.testing.connectionFailure
|
||||
import org.thoughtcrime.securesms.testing.failure
|
||||
import org.thoughtcrime.securesms.testing.parsedRequestBody
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.thoughtcrime.securesms.testing.timeout
|
||||
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.internal.push.MismatchedDevices
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyState
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ChangeNumberViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
ThreadUtil.runOnMainSync {
|
||||
viewModel = ChangeNumberViewModel(
|
||||
localNumber = harness.self.requireE164(),
|
||||
changeNumberRepository = ChangeNumberRepository(),
|
||||
savedState = SavedStateHandle(),
|
||||
password = SignalStore.account().servicePassword!!,
|
||||
verifyAccountRepository = VerifyAccountRepository(harness.application)
|
||||
)
|
||||
|
||||
viewModel.setNewCountry(1)
|
||||
viewModel.setNewNationalNumber("5555550102")
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* If we encounter a server error, this means the server ack our request and rejected it. In this
|
||||
* case we know the change *did not* take on the server and can reset to a clean state.
|
||||
*/
|
||||
@Test
|
||||
fun testChangeNumber_givenServerFailedApiCall() {
|
||||
// GIVEN
|
||||
val oldPni = Recipient.self().requirePni()
|
||||
val oldE164 = Recipient.self().requireE164()
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { MockResponse().failure(500) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
|
||||
|
||||
// THEN
|
||||
processor.isServerSentError() assertIs true
|
||||
Recipient.self().requireE164() assertIs oldE164
|
||||
Recipient.self().requirePni() assertIs oldPni
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
|
||||
* number on the server side. We have to do a whoami call to query the server for our details and then
|
||||
* respond accordingly.
|
||||
*
|
||||
* In this case, the whoami is our old details, so we can know the change *did not* take on the server
|
||||
* and can reset to a clean state.
|
||||
*/
|
||||
@Test
|
||||
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToServer() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val oldPni = Recipient.self().requirePni()
|
||||
val oldE164 = Recipient.self().requireE164()
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { MockResponse().connectionFailure() },
|
||||
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
|
||||
|
||||
// THEN
|
||||
processor.isServerSentError() assertIs false
|
||||
Recipient.self().requireE164() assertIs oldE164
|
||||
Recipient.self().requirePni() assertIs oldPni
|
||||
SignalStore.misc().isChangeNumberLocked assertIs false
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
|
||||
* number on the server side. We have to do a whoami call to query the server for our details and then
|
||||
* respond accordingly.
|
||||
*
|
||||
* In this case, the whoami is our new details, so we can know the change *did* take on the server
|
||||
* and need to keep the app in a locked state. The test then uses the ChangeNumberLockActivity to unlock
|
||||
* and apply the pending state after confirming the change on the server.
|
||||
*/
|
||||
@Test
|
||||
@FlakyTest
|
||||
@Ignore("Test sometimes requires manual intervention to continue.")
|
||||
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val oldPni = Recipient.self().requirePni()
|
||||
val oldE164 = Recipient.self().requireE164()
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
MockResponse().timeout()
|
||||
},
|
||||
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, newPni, "+15555550102")) },
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
|
||||
|
||||
// THEN
|
||||
processor.isServerSentError() assertIs false
|
||||
Recipient.self().requireE164() assertIs oldE164
|
||||
Recipient.self().requirePni() assertIs oldPni
|
||||
SignalStore.misc().isChangeNumberLocked assertIs true
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNotNull()
|
||||
|
||||
// WHEN AGAIN Processing lock
|
||||
val scenario = harness.launchActivity<ChangeNumberLockActivity>()
|
||||
scenario.onActivity {}
|
||||
ThreadUtil.sleep(500)
|
||||
|
||||
// THEN AGAIN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
|
||||
MockResponse().failure(423, MockProvider.lockedFailure)
|
||||
} else {
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
}
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
|
||||
processor.registrationLock() assertIs true
|
||||
Recipient.self().requirePni() assertIsNot newPni
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
|
||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
if (changeNumberRequest.deviceMessages.isEmpty()) {
|
||||
MockResponse().failure(
|
||||
409,
|
||||
MismatchedDevices().apply {
|
||||
missingDevices = listOf(2)
|
||||
extraDevices = emptyList()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
}
|
||||
},
|
||||
Get("/v2/keys/$aci/2") {
|
||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = PNI.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
|
||||
Put("/v2/accounts/number") { r ->
|
||||
changeNumberRequest = r.parsedRequestBody()
|
||||
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
|
||||
MockResponse().failure(423, MockProvider.lockedFailure)
|
||||
} else if (changeNumberRequest.deviceMessages.isEmpty()) {
|
||||
MockResponse().failure(
|
||||
409,
|
||||
MismatchedDevices().apply {
|
||||
missingDevices = listOf(2)
|
||||
extraDevices = emptyList()
|
||||
}
|
||||
)
|
||||
} else if (changeNumberRequest.deviceMessages.size == 1) {
|
||||
MockResponse().failure(
|
||||
409,
|
||||
MismatchedDevices().apply {
|
||||
missingDevices = listOf(2, 3)
|
||||
extraDevices = emptyList()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
|
||||
}
|
||||
},
|
||||
Get("/v2/keys/$aci/2") {
|
||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
|
||||
},
|
||||
Get("/v2/keys/$aci/3") {
|
||||
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 3))
|
||||
},
|
||||
Put("/v2/keys") { r ->
|
||||
setPreKeysRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
|
||||
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
|
||||
processor.registrationLock() assertIs true
|
||||
Recipient.self().requirePni() assertIsNot newPni
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
|
||||
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
|
||||
|
||||
// THEN
|
||||
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
|
||||
}
|
||||
|
||||
private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) {
|
||||
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
|
||||
val pniMetadataStore = SignalStore.account().pniPreKeys
|
||||
|
||||
Recipient.self().requireE164() assertIs "+15555550102"
|
||||
Recipient.self().requirePni() assertIs newPni
|
||||
|
||||
SignalStore.account().pniRegistrationId assertIs changeNumberRequest.pniRegistrationIds["1"]!!
|
||||
SignalStore.account().pniIdentityKey.publicKey assertIs changeNumberRequest.pniIdentityKey
|
||||
pniMetadataStore.activeSignedPreKeyId assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.keyId
|
||||
|
||||
val activeSignedPreKey: SignedPreKeyRecord = pniProtocolStore.loadSignedPreKey(pniMetadataStore.activeSignedPreKeyId)
|
||||
activeSignedPreKey.keyPair.publicKey assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.publicKey
|
||||
activeSignedPreKey.signature assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.signature
|
||||
|
||||
setPreKeysRequest.signedPreKey.publicKey assertIs activeSignedPreKey.keyPair.publicKey
|
||||
setPreKeysRequest.preKeys assertIsSize 100
|
||||
|
||||
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
|
||||
}
|
||||
}
|
||||
@@ -154,7 +154,8 @@ class ConversationItemPreviewer {
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
System.currentTimeMillis()
|
||||
System.currentTimeMillis(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.database.IdentityTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
|
||||
@@ -64,7 +64,7 @@ class SafetyNumberChangeDialogPreviewer {
|
||||
scenario.onActivity { conversationActivity ->
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestinations(
|
||||
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
|
||||
identityRecords = AppDependencies.protocolStore.aci().identities().getIdentityRecords(othersRecipients).identityRecords,
|
||||
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
|
||||
)
|
||||
.show(conversationActivity.supportFragmentManager)
|
||||
|
||||
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import com.bumptech.glide.RequestManager
|
||||
import io.mockk.mockk
|
||||
@@ -203,8 +204,8 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
private val colorizer = Colorizer()
|
||||
|
||||
override val lifecycleOwner: LifecycleOwner = mockk(relaxed = true)
|
||||
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.Standard
|
||||
|
||||
override val clickListener: ConversationAdapter.ItemClickListener = FakeConversationItemClickListener
|
||||
override val selectedItems: Set<MultiselectPart> = emptySet()
|
||||
override val isMessageRequestAccepted: Boolean = true
|
||||
@@ -313,7 +314,7 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit
|
||||
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit
|
||||
|
||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
|
||||
|
||||
@@ -330,5 +331,6 @@ class V2ConversationItemShapeTest {
|
||||
override fun onMessageRequestAcceptOptionsClicked() = Unit
|
||||
|
||||
override fun onItemDoubleClick(item: MultiselectPart) = Unit
|
||||
override fun onPaymentTombstoneClicked() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaName
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MediaStream
|
||||
@@ -25,6 +26,8 @@ import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.backup.MediaId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
@@ -47,9 +50,9 @@ class AttachmentTableTest_deduping {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalStore.account().setAci(ServiceId.ACI.from(UUID.randomUUID()))
|
||||
SignalStore.account().setPni(ServiceId.PNI.from(UUID.randomUUID()))
|
||||
SignalStore.account().setE164("+15558675309")
|
||||
SignalStore.account.setAci(ServiceId.ACI.from(UUID.randomUUID()))
|
||||
SignalStore.account.setPni(ServiceId.PNI.from(UUID.randomUUID()))
|
||||
SignalStore.account.setE164("+15558675309")
|
||||
|
||||
SignalDatabase.attachments.deleteAllAttachments()
|
||||
}
|
||||
@@ -195,6 +198,8 @@ class AttachmentTableTest_deduping {
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Mimics sending two files at once. Ensures all fields are kept in sync as we compress and upload.
|
||||
@@ -220,6 +225,7 @@ class AttachmentTableTest_deduping {
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Re-use the upload when uploaded recently
|
||||
@@ -234,6 +240,7 @@ class AttachmentTableTest_deduping {
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
@@ -253,6 +260,7 @@ class AttachmentTableTest_deduping {
|
||||
assertSkipTransform(id2, true)
|
||||
|
||||
assertDoesNotHaveRemoteFields(id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This isn't so much "desirable behavior" as it is documenting how things work.
|
||||
@@ -282,6 +290,7 @@ class AttachmentTableTest_deduping {
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you edited a video, sent it, then forwarded it. We should match, skip transform, and skip upload.
|
||||
@@ -297,6 +306,7 @@ class AttachmentTableTest_deduping {
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you edited a video, sent it, then forwarded it, but *edited the forwarded video*. We should not dedupe.
|
||||
@@ -327,6 +337,7 @@ class AttachmentTableTest_deduping {
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you sent an image using high quality, then forwarded it using standard quality.
|
||||
@@ -343,6 +354,7 @@ class AttachmentTableTest_deduping {
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Make sure that files marked as unhashable are all updated together
|
||||
@@ -457,6 +469,7 @@ class AttachmentTableTest_deduping {
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Making sure things work for quotes of videos, which have trickier transform properties
|
||||
@@ -470,6 +483,7 @@ class AttachmentTableTest_deduping {
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,6 +662,15 @@ class AttachmentTableTest_deduping {
|
||||
|
||||
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createPointerAttachment(attachmentId, uploadTimestamp), uploadTimestamp)
|
||||
|
||||
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
SignalDatabase.attachments.setArchiveData(
|
||||
attachmentId = attachmentId,
|
||||
archiveCdn = Cdn.CDN_3.cdnNumber,
|
||||
archiveMediaName = attachment.getMediaName().name,
|
||||
archiveThumbnailMediaId = MediaId(Util.getSecretBytes(15)).encode(),
|
||||
archiveMediaId = MediaId(Util.getSecretBytes(15)).encode()
|
||||
)
|
||||
}
|
||||
|
||||
fun delete(attachmentId: AttachmentId) {
|
||||
@@ -746,6 +769,15 @@ class AttachmentTableTest_deduping {
|
||||
assertEquals(lhsAttachment.cdn.cdnNumber, rhsAttachment.cdn.cdnNumber)
|
||||
}
|
||||
|
||||
fun assertArchiveFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
|
||||
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
|
||||
|
||||
assertEquals(lhsAttachment.archiveCdn, rhsAttachment.archiveCdn)
|
||||
assertEquals(lhsAttachment.archiveMediaName, rhsAttachment.archiveMediaName)
|
||||
assertEquals(lhsAttachment.archiveMediaId, rhsAttachment.archiveMediaId)
|
||||
}
|
||||
|
||||
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
assertEquals(0, databaseAttachment.uploadTimestamp)
|
||||
@@ -792,7 +824,8 @@ class AttachmentTableTest_deduping {
|
||||
uploadTimestamp,
|
||||
databaseAttachment.caption,
|
||||
databaseAttachment.stickerLocator,
|
||||
databaseAttachment.blurHash
|
||||
databaseAttachment.blurHash,
|
||||
databaseAttachment.uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class CallTableTest {
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
SignalDatabase.calls.deleteGroupCall(call!!)
|
||||
SignalDatabase.calls.markCallDeletedFromSyncEvent(call!!)
|
||||
|
||||
val deletedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
|
||||
@@ -69,9 +69,10 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
||||
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Type.GROUP_CALL,
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
@@ -438,11 +439,12 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
||||
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
|
||||
callId = callId,
|
||||
recipientId = groupRecipientId,
|
||||
direction = CallTable.Direction.INCOMING,
|
||||
timestamp = System.currentTimeMillis()
|
||||
timestamp = System.currentTimeMillis(),
|
||||
type = CallTable.Type.GROUP_CALL
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.signal.core.util.getIndexes
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
/**
|
||||
@@ -30,7 +30,7 @@ class DatabaseConsistencyTest {
|
||||
@Test
|
||||
fun testUpgradeConsistency() {
|
||||
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
|
||||
val testHelper = InMemoryTestHelper(ApplicationDependencies.getApplication()).also {
|
||||
val testHelper = InMemoryTestHelper(AppDependencies.application).also {
|
||||
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@@ -26,7 +26,7 @@ class DatabaseObserverTest {
|
||||
@Before
|
||||
fun setup() {
|
||||
db = SignalDatabase.instance!!.signalWritableDatabase
|
||||
observer = ApplicationDependencies.getDatabaseObserver()
|
||||
observer = AppDependencies.databaseObserver
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -25,15 +25,6 @@ class DistributionListTablesTest {
|
||||
Assert.assertNotNull(id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createList_whenNameConflict_failToInsert() {
|
||||
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNotNull(id)
|
||||
|
||||
val id2: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
|
||||
Assert.assertNull(id2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getList_returnCorrectList() {
|
||||
createRecipients(3)
|
||||
|
||||
@@ -167,8 +167,8 @@ class GroupTableTest {
|
||||
|
||||
@Test
|
||||
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
|
||||
val g1 = insertPushGroup(listOf())
|
||||
val g2 = insertPushGroup(listOf())
|
||||
val g1 = insertPushGroup(members = emptyList())
|
||||
val g2 = insertPushGroup(members = emptyList())
|
||||
|
||||
val gr1 = groupTable.getGroup(g1)
|
||||
val gr2 = groupTable.getGroup(g2)
|
||||
@@ -195,6 +195,85 @@ class GroupTableTest {
|
||||
assertEquals(groups[0].id, groupInCommon)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForTheSharedToken_thenIExpectBothGroups() {
|
||||
insertPushGroup("Group Alice")
|
||||
insertPushGroup("Group Bob")
|
||||
|
||||
SignalDatabase.groups.queryGroupsByTitle(
|
||||
inputQuery = "Group",
|
||||
includeInactive = false,
|
||||
excludeV1 = false,
|
||||
excludeMms = false
|
||||
).use {
|
||||
assertEquals(2, it.cursor?.count)
|
||||
|
||||
val firstGroup = it.getNext()
|
||||
val secondGroup = it.getNext()
|
||||
|
||||
assertEquals("Group Alice", firstGroup?.title)
|
||||
assertEquals("Group Bob", secondGroup?.title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForAnUnsharedToken_thenIExpectOneGroup() {
|
||||
insertPushGroup("Group Alice")
|
||||
insertPushGroup("Group Bob")
|
||||
|
||||
SignalDatabase.groups.queryGroupsByTitle(
|
||||
inputQuery = "Alice",
|
||||
includeInactive = false,
|
||||
excludeV1 = false,
|
||||
excludeMms = false
|
||||
).use {
|
||||
assertEquals(1, it.cursor?.count)
|
||||
|
||||
val firstGroup = it.getNext()
|
||||
|
||||
assertEquals("Group Alice", firstGroup?.title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupWithThreeTokens_whenISearchForTheFirstAndLastToken_thenIExpectThatGroup() {
|
||||
insertPushGroup("Group & Alice")
|
||||
|
||||
SignalDatabase.groups.queryGroupsByTitle(
|
||||
inputQuery = "Group Alice",
|
||||
includeInactive = false,
|
||||
excludeV1 = false,
|
||||
excludeMms = false
|
||||
).use {
|
||||
assertEquals(1, it.cursor?.count)
|
||||
|
||||
val firstGroup = it.getNext()
|
||||
|
||||
assertEquals("Group & Alice", firstGroup?.title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoGroupsWithSharedTokens_whenISearchForAnExactMatch_thenIExpectThatGroupFirst() {
|
||||
insertPushGroup("Group Alice Bob")
|
||||
insertPushGroup("Group Bob")
|
||||
|
||||
SignalDatabase.groups.queryGroupsByTitle(
|
||||
inputQuery = "Group Bob",
|
||||
includeInactive = false,
|
||||
excludeV1 = false,
|
||||
excludeMms = false
|
||||
).use {
|
||||
assertEquals(2, it.cursor?.count)
|
||||
|
||||
val firstGroup = it.getNext()
|
||||
val second = it.getNext()
|
||||
|
||||
assertEquals("Group Bob", firstGroup?.title)
|
||||
assertEquals("Group Alice Bob", second?.title)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertThread(groupId: GroupId): Long {
|
||||
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
|
||||
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
|
||||
@@ -214,6 +293,7 @@ class GroupTableTest {
|
||||
}
|
||||
|
||||
private fun insertPushGroup(
|
||||
title: String = "Test Group",
|
||||
members: List<DecryptedMember> = listOf(
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(harness.self.requireAci().toByteString())
|
||||
@@ -229,6 +309,7 @@ class GroupTableTest {
|
||||
): GroupId {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
val decryptedGroupState = DecryptedGroup.Builder()
|
||||
.title(title)
|
||||
.members(members)
|
||||
.revision(0)
|
||||
.build()
|
||||
|
||||
@@ -159,7 +159,7 @@ class KyberPreKeyTableTest {
|
||||
val count = SignalDatabase.rawDatabase
|
||||
.update(KyberPreKeyTable.TABLE_NAME)
|
||||
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
|
||||
assertEquals(1, count)
|
||||
@@ -169,8 +169,15 @@ class KyberPreKeyTableTest {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select(KyberPreKeyTable.STALE_TIMESTAMP)
|
||||
.from(KyberPreKeyTable.TABLE_NAME)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
|
||||
}
|
||||
|
||||
private fun ServiceId.toAccountId(): String {
|
||||
return when (this) {
|
||||
is ACI -> this.toString()
|
||||
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.updateAll
|
||||
import org.thoughtcrime.securesms.crash.CrashConfig
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
|
||||
class LogDatabaseTest {
|
||||
|
||||
private val db: LogDatabase = LogDatabase.getInstance(ApplicationDependencies.getApplication())
|
||||
private val db: LogDatabase = LogDatabase.getInstance(AppDependencies.application)
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNamePattern() {
|
||||
|
||||
@@ -30,8 +30,8 @@ class MessageTableTest_gifts {
|
||||
|
||||
mms.deleteAllThreads()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
SignalStore.account.setAci(localAci)
|
||||
SignalStore.account.setPni(localPni)
|
||||
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
|
||||
}
|
||||
|
||||
@@ -40,14 +40,14 @@ class MmsTableTest_stories {
|
||||
|
||||
mms.deleteAllThreads()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
SignalStore.account.setAci(localAci)
|
||||
SignalStore.account.setPni(localPni)
|
||||
|
||||
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
|
||||
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
|
||||
|
||||
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelRecipient.id)
|
||||
SignalStore.releaseChannel.setReleaseChannelRecipientId(releaseChannelRecipient.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -120,7 +120,7 @@ class OneTimePreKeyTableTest {
|
||||
val count = SignalDatabase.rawDatabase
|
||||
.update(OneTimePreKeyTable.TABLE_NAME)
|
||||
.values(OneTimePreKeyTable.STALE_TIMESTAMP to staleTime)
|
||||
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
|
||||
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
|
||||
assertEquals(1, count)
|
||||
@@ -130,8 +130,15 @@ class OneTimePreKeyTableTest {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select(OneTimePreKeyTable.STALE_TIMESTAMP)
|
||||
.from(OneTimePreKeyTable.TABLE_NAME)
|
||||
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
|
||||
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
.readToSingleObject { it.requireLongOrNull(OneTimePreKeyTable.STALE_TIMESTAMP) }
|
||||
}
|
||||
|
||||
private fun ServiceId.toAccountId(): String {
|
||||
return when (this) {
|
||||
is ACI -> this.toString()
|
||||
is PNI -> OneTimePreKeyTable.PNI_ACCOUNT_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||
@@ -28,7 +28,7 @@ class RecipientTableTest_applyStorageSyncContactUpdate {
|
||||
@Test
|
||||
fun insertMessageOnVerifiedToDefault() {
|
||||
// GIVEN
|
||||
val identities = ApplicationDependencies.getProtocolStore().aci().identities()
|
||||
val identities = AppDependencies.protocolStore.aci().identities()
|
||||
val other = Recipient.resolved(harness.others[0])
|
||||
|
||||
MmsHelper.insert(recipient = other)
|
||||
|
||||
@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
@@ -53,9 +53,9 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
SignalStore.account().setE164(E164_SELF)
|
||||
SignalStore.account().setAci(ACI_SELF)
|
||||
SignalStore.account().setPni(PNI_SELF)
|
||||
SignalStore.account.setE164(E164_SELF)
|
||||
SignalStore.account.setAci(ACI_SELF)
|
||||
SignalStore.account.setPni(PNI_SELF)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1113,8 +1113,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
|
||||
}
|
||||
|
||||
ApplicationDependencies.getRecipientCache().clear()
|
||||
ApplicationDependencies.getRecipientCache().clearSelf()
|
||||
AppDependencies.recipientCache.clear()
|
||||
AppDependencies.recipientCache.clearSelf()
|
||||
RecipientId.clearCache()
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
|
||||
@@ -44,8 +46,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
recipients = SignalDatabase.recipients
|
||||
sms = SignalDatabase.messages
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
SignalStore.account.setAci(localAci)
|
||||
SignalStore.account.setPni(localPni)
|
||||
|
||||
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
|
||||
bob = recipients.getOrInsertFromServiceId(bobServiceId)
|
||||
@@ -286,11 +288,18 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
|
||||
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingMessage {
|
||||
wallClock++
|
||||
|
||||
val updateDescription = GV2UpdateDescription(
|
||||
gv2ChangeDescription = groupContext,
|
||||
groupChangeUpdate = GroupsV2UpdateMessageConverter.translateDecryptedChangeUpdate(SignalStore.account.getServiceIds(), groupContext)
|
||||
)
|
||||
|
||||
return IncomingMessage.groupUpdate(
|
||||
from = sender,
|
||||
timestamp = wallClock,
|
||||
groupId = groupId,
|
||||
groupContext = groupContext,
|
||||
update = updateDescription,
|
||||
isGroupAdd = false,
|
||||
serverGuid = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.audio.AudioHash
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import java.util.UUID
|
||||
|
||||
object UriAttachmentBuilder {
|
||||
fun build(
|
||||
@@ -22,23 +23,28 @@ object UriAttachmentBuilder {
|
||||
stickerLocator: StickerLocator? = null,
|
||||
blurHash: BlurHash? = null,
|
||||
audioHash: AudioHash? = null,
|
||||
transformProperties: AttachmentTable.TransformProperties? = null
|
||||
transformProperties: AttachmentTable.TransformProperties? = null,
|
||||
uuid: UUID? = UUID.randomUUID()
|
||||
): UriAttachment {
|
||||
return UriAttachment(
|
||||
uri,
|
||||
contentType,
|
||||
transferState,
|
||||
size,
|
||||
fileName,
|
||||
voiceNote,
|
||||
borderless,
|
||||
videoGif,
|
||||
quote,
|
||||
caption,
|
||||
stickerLocator,
|
||||
blurHash,
|
||||
audioHash,
|
||||
transformProperties
|
||||
dataUri = uri,
|
||||
contentType = contentType,
|
||||
transferState = transferState,
|
||||
size = size,
|
||||
width = 0,
|
||||
height = 0,
|
||||
fileName = fileName,
|
||||
fastPreflightId = null,
|
||||
voiceNote = voiceNote,
|
||||
borderless = borderless,
|
||||
videoGif = videoGif,
|
||||
quote = quote,
|
||||
caption = caption,
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
audioHash = audioHash,
|
||||
transformProperties = transformProperties,
|
||||
uuid = uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toDecimalValue
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.FiatValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class FixInAppCurrencyIfAbleTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
|
||||
|
||||
@Test
|
||||
fun givenNoSubscribers_whenIMigrate_thenIDoNothing() {
|
||||
migrate()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASubscriberButNoPayment_whenIMigrate_thenIDoNothing() {
|
||||
val subscriber = insertSubscriber("USD")
|
||||
clearCurrencyCode(subscriber)
|
||||
migrate()
|
||||
|
||||
getCurrencyCode(subscriber) assertIs ""
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASubscriberAndMismatchedPayment_whenIMigrate_thenIDoNothing() {
|
||||
val subscriber = insertSubscriber("USD")
|
||||
val otherSubscriber = insertSubscriber("EUR")
|
||||
insertPayment(otherSubscriber)
|
||||
clearCurrencyCode(subscriber)
|
||||
migrate()
|
||||
|
||||
getCurrencyCode(subscriber) assertIs ""
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASubscriberAndPaymentWithNoSubscriber_whenIMigrate_thenDoNothing() {
|
||||
val subscriber = insertSubscriber("USD")
|
||||
insertPayment(null)
|
||||
clearCurrencyCode(subscriber)
|
||||
migrate()
|
||||
|
||||
getCurrencyCode(subscriber) assertIs ""
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASubscriberAndMatchingPayment_whenIMigrate_thenUpdateCurrencyCode() {
|
||||
val subscriber = insertSubscriber("USD")
|
||||
insertPayment(subscriber)
|
||||
clearCurrencyCode(subscriber)
|
||||
migrate()
|
||||
|
||||
getCurrencyCode(subscriber) assertIs "USD"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenASupercededSubscriber_whenIMigrate_thenIDoNothing() {
|
||||
val oldSubscriber = insertSubscriber("USD")
|
||||
insertPayment(oldSubscriber)
|
||||
clearCurrencyCode(oldSubscriber)
|
||||
insertSubscriber("USD")
|
||||
migrate()
|
||||
}
|
||||
|
||||
private fun migrate() {
|
||||
V236_FixInAppSubscriberCurrencyIfAble.migrate(
|
||||
context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
|
||||
db = SignalDatabase.rawDatabase,
|
||||
oldVersion = 0,
|
||||
newVersion = 0
|
||||
)
|
||||
}
|
||||
|
||||
private fun insertSubscriber(currencyCode: String): InAppPaymentSubscriberRecord {
|
||||
val record = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = Currency.getInstance(currencyCode),
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(record)
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
private fun clearCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord) {
|
||||
SignalDatabase.rawDatabase.update(InAppPaymentSubscriberTable.TABLE_NAME)
|
||||
.values(InAppPaymentSubscriberTable.CURRENCY_CODE to "")
|
||||
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
|
||||
.run()
|
||||
}
|
||||
|
||||
private fun getCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord): String {
|
||||
return SignalDatabase.rawDatabase.select(InAppPaymentSubscriberTable.CURRENCY_CODE)
|
||||
.from(InAppPaymentSubscriberTable.TABLE_NAME)
|
||||
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
|
||||
.run()
|
||||
.readToSingleObject { it.requireNonNullString(InAppPaymentSubscriberTable.CURRENCY_CODE) }!!
|
||||
}
|
||||
|
||||
private fun insertPayment(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord?): InAppPaymentTable.InAppPayment {
|
||||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_DONATION,
|
||||
state = InAppPaymentTable.State.END,
|
||||
subscriberId = inAppPaymentSubscriberRecord?.subscriberId,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
amount = FiatValue(
|
||||
currencyCode = inAppPaymentSubscriberRecord?.currency?.currencyCode ?: "USD",
|
||||
amount = BigDecimal.ONE.toDecimalValue()
|
||||
),
|
||||
level = 200,
|
||||
paymentMethodType = inAppPaymentSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
)
|
||||
)
|
||||
|
||||
return SignalDatabase.inAppPayments.getById(id)!!
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.dependencies
|
||||
|
||||
import android.app.Application
|
||||
import io.mockk.spyk
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
@@ -23,6 +24,9 @@ import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.Verb
|
||||
import org.thoughtcrime.securesms.testing.runSync
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.api.SignalServiceDataStore
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender
|
||||
import org.whispersystems.signalservice.api.SignalWebSocket
|
||||
import org.whispersystems.signalservice.api.push.TrustStore
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
|
||||
@@ -37,12 +41,13 @@ import java.util.Optional
|
||||
*
|
||||
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess].
|
||||
*/
|
||||
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
|
||||
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : AppDependencies.Provider by default {
|
||||
|
||||
private val serviceTrustStore: TrustStore
|
||||
private val uncensoredConfiguration: SignalServiceConfiguration
|
||||
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
|
||||
private val recipientCache: LiveRecipientCache
|
||||
private var signalServiceMessageSender: SignalServiceMessageSender? = null
|
||||
|
||||
init {
|
||||
runSync {
|
||||
@@ -53,18 +58,21 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
|
||||
Get("/v1/websocket/?login=") {
|
||||
MockResponse().success().withWebSocketUpgrade(mockIdentifiedWebSocket)
|
||||
},
|
||||
Get("/v1/websocket", { !it.path.contains("login") }) {
|
||||
Get("/v1/websocket", {
|
||||
val path = it.path
|
||||
return@Get path == null || !path.contains("login")
|
||||
}) {
|
||||
MockResponse().success().withWebSocketUpgrade(object : WebSocketListener() {})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
webServer.setDispatcher(object : Dispatcher() {
|
||||
webServer.dispatcher = object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
val handler = handlers.firstOrNull { it.requestPredicate(request) }
|
||||
return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
serviceTrustStore = SignalServiceTrustStore(application)
|
||||
uncensoredConfiguration = SignalServiceConfiguration(
|
||||
@@ -101,6 +109,17 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
|
||||
return recipientCache
|
||||
}
|
||||
|
||||
override fun provideSignalServiceMessageSender(
|
||||
signalWebSocket: SignalWebSocket,
|
||||
protocolStore: SignalServiceDataStore,
|
||||
signalServiceConfiguration: SignalServiceConfiguration
|
||||
): SignalServiceMessageSender {
|
||||
if (signalServiceMessageSender == null) {
|
||||
signalServiceMessageSender = spyk(objToCopy = default.provideSignalServiceMessageSender(signalWebSocket, protocolStore, signalServiceConfiguration))
|
||||
}
|
||||
return signalServiceMessageSender!!
|
||||
}
|
||||
|
||||
class MockWebSocket : WebSocketListener() {
|
||||
private val TAG = "MockWebSocket"
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
@@ -38,7 +38,7 @@ class AttachmentCompressionJobTest {
|
||||
StreamUtil.readFully(it)
|
||||
}
|
||||
|
||||
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(ApplicationDependencies.getApplication())
|
||||
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(AppDependencies.application)
|
||||
|
||||
val firstPreUpload = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
|
||||
val firstDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(firstPreUpload)
|
||||
@@ -51,12 +51,12 @@ class AttachmentCompressionJobTest {
|
||||
|
||||
val secondJobLatch = CountDownLatch(1)
|
||||
val jobThread = Thread {
|
||||
firstCompressionJob.setContext(ApplicationDependencies.getApplication())
|
||||
firstCompressionJob.setContext(AppDependencies.application)
|
||||
firstJobResult = firstCompressionJob.run()
|
||||
|
||||
secondJobLatch.await()
|
||||
|
||||
secondCompressionJob!!.setContext(ApplicationDependencies.getApplication())
|
||||
secondCompressionJob!!.setContext(AppDependencies.application)
|
||||
secondJobResult = secondCompressionJob!!.run()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.net.Uri
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
|
||||
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Makes inserting messages through the "normal" code paths simpler. Mostly focused on incoming messages.
|
||||
*/
|
||||
class MessageHelper(private val harness: SignalActivityRule, var startTime: Long = System.currentTimeMillis()) {
|
||||
|
||||
val alice: RecipientId = harness.others[0]
|
||||
val bob: RecipientId = harness.others[1]
|
||||
val group: GroupTestingUtils.TestGroupInfo = harness.group!!
|
||||
val processor: MessageContentProcessor = MessageContentProcessor(harness.context)
|
||||
|
||||
init {
|
||||
val threadIdSlot = slot<Long>()
|
||||
mockkStatic(ThreadUpdateJob::class)
|
||||
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
|
||||
SignalDatabase.threads.update(threadIdSlot.captured, false)
|
||||
}
|
||||
}
|
||||
|
||||
fun tearDown() {
|
||||
unmockkStatic(ThreadUpdateJob::class)
|
||||
}
|
||||
|
||||
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
val messageData = MessageData(author = sender, timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null,
|
||||
allowExpireTimeChanges = false
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun outgoingText(conversationId: RecipientId = alice, successfulSend: Boolean = true, updateMessage: ((OutgoingMessage) -> OutgoingMessage)? = null): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
|
||||
val threadRecipient = Recipient.resolved(conversationId)
|
||||
|
||||
val message = OutgoingMessage(
|
||||
threadRecipient = threadRecipient,
|
||||
body = MessageContentFuzzer.string(),
|
||||
sentTimeMillis = messageData.timestamp,
|
||||
isUrgent = true,
|
||||
isSecure = true
|
||||
).let { updateMessage?.invoke(it) ?: it }
|
||||
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
|
||||
|
||||
if (successfulSend) {
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
}
|
||||
|
||||
return messageData.copy(messageId = messageId)
|
||||
}
|
||||
|
||||
fun outgoingMessage(conversationId: RecipientId = alice, updateMessage: OutgoingMessage.() -> OutgoingMessage): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
|
||||
val threadRecipient = Recipient.resolved(conversationId)
|
||||
|
||||
val message = OutgoingMessage(
|
||||
threadRecipient = threadRecipient,
|
||||
sentTimeMillis = messageData.timestamp,
|
||||
isUrgent = true,
|
||||
isSecure = true
|
||||
).apply { updateMessage() }
|
||||
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
|
||||
|
||||
return messageData.copy(messageId = messageId)
|
||||
}
|
||||
|
||||
fun outgoingAttachment(data: ByteArray, uuid: UUID? = UUID.randomUUID()): Attachment {
|
||||
val uri: Uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
||||
|
||||
val attachment: UriAttachment = UriAttachmentBuilder.build(
|
||||
id = Random.nextLong(),
|
||||
uri = uri,
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
transformProperties = AttachmentTable.TransformProperties(),
|
||||
uuid = uuid
|
||||
)
|
||||
|
||||
return attachment
|
||||
}
|
||||
|
||||
fun outgoingGroupChange(): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
|
||||
val groupRecipient = Recipient.resolved(group.recipientId)
|
||||
val decryptedGroupV2Context = DecryptedGroupV2Context(
|
||||
context = group.groupV2Context,
|
||||
groupState = SignalDatabase.groups.getGroup(group.groupId).get().requireV2GroupProperties().decryptedGroup
|
||||
)
|
||||
|
||||
val updateDescription = GV2UpdateDescription.Builder()
|
||||
.gv2ChangeDescription(decryptedGroupV2Context)
|
||||
.groupChangeUpdate(GroupsV2UpdateMessageConverter.translateDecryptedChange(SignalStore.account.getServiceIds(), decryptedGroupV2Context))
|
||||
.build()
|
||||
|
||||
val outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, startTime)
|
||||
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(messageId, true)
|
||||
|
||||
return messageData.copy(messageId = messageId)
|
||||
}
|
||||
|
||||
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
val messageData = MessageData(author = sender, timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzStickerMediaMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime = nextStartTime()
|
||||
|
||||
val messageData = MessageData(author = sender, timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.editTextMessage(
|
||||
targetTimestamp = targetTimestamp,
|
||||
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
).dataMessage!!
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
|
||||
startTime = nextStartTime()
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncDeleteForMeMessage(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
|
||||
startTime = nextStartTime()
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncDeleteForMeMessage(deletes.toList()),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncDeleteForMeConversation(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
|
||||
startTime = nextStartTime()
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncDeleteForMeConversation(deletes.toList()),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncDeleteForMeLocalOnlyConversation(vararg conversations: RecipientId): MessageData {
|
||||
startTime = nextStartTime()
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncDeleteForMeLocalOnlyConversation(conversations.toList()),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): MessageData {
|
||||
startTime = nextStartTime()
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncDeleteForMeAttachment(conversationId, message, uuid, digest, plainTextHash),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next "sentTimestamp" for current + [nextMessageOffset]th message. Useful for early message processing and future message timestamps.
|
||||
*/
|
||||
fun nextStartTime(nextMessageOffset: Int = 1): Long {
|
||||
return startTime + 1000 * nextMessageOffset
|
||||
}
|
||||
|
||||
data class MessageData(
|
||||
val author: RecipientId = RecipientId.UNKNOWN,
|
||||
val serverGuid: UUID = UUID.randomUUID(),
|
||||
val timestamp: Long,
|
||||
val messageId: Long = -1L
|
||||
)
|
||||
}
|
||||
@@ -6,23 +6,14 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -31,43 +22,28 @@ class SyncMessageProcessorTest_readSyncs {
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private lateinit var bob: RecipientId
|
||||
private lateinit var group: GroupTestingUtils.TestGroupInfo
|
||||
private lateinit var processor: MessageContentProcessor
|
||||
private lateinit var messageHelper: MessageHelper
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
alice = harness.others[0]
|
||||
bob = harness.others[1]
|
||||
group = harness.group!!
|
||||
|
||||
processor = MessageContentProcessor(harness.context)
|
||||
|
||||
val threadIdSlot = slot<Long>()
|
||||
mockkStatic(ThreadUpdateJob::class)
|
||||
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
|
||||
SignalDatabase.threads.update(threadIdSlot.captured, false)
|
||||
}
|
||||
messageHelper = MessageHelper(harness)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic(ThreadUpdateJob::class)
|
||||
messageHelper.tearDown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadMessage() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp, alice to message2Timestamp)
|
||||
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
@@ -75,16 +51,14 @@ class SyncMessageProcessorTest_readSyncs {
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadMessageMissingTimestamp() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message2Timestamp)
|
||||
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
@@ -92,21 +66,19 @@ class SyncMessageProcessorTest_readSyncs {
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEdits() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp)
|
||||
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
|
||||
|
||||
val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp
|
||||
val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp
|
||||
|
||||
val message2Timestamp = messageHelper.incomingMedia().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
|
||||
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
@@ -114,112 +86,22 @@ class SyncMessageProcessorTest_readSyncs {
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEditsInGroup() {
|
||||
val messageHelper = MessageHelper()
|
||||
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText(sender = alice, destination = group.recipientId).timestamp
|
||||
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
|
||||
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp)
|
||||
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||
|
||||
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = alice, destination = group.recipientId).timestamp
|
||||
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = alice, destination = group.recipientId).timestamp
|
||||
val message2Timestamp = messageHelper.incomingMedia(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
|
||||
|
||||
val message2Timestamp = messageHelper.incomingMedia(sender = bob, destination = group.recipientId).timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(group.recipientId)!!
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(bob to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
|
||||
messageHelper.syncReadMessage(messageHelper.bob to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
private inner class MessageHelper(var startTime: Long = System.currentTimeMillis()) {
|
||||
|
||||
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzStickerMediaMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.editTextMessage(
|
||||
targetTimestamp = targetTimestamp,
|
||||
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
).dataMessage!!
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
|
||||
startTime += 1000
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
}
|
||||
|
||||
private data class MessageData(val serverGuid: UUID = UUID.randomUUID(), val timestamp: Long)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.hamcrest.Matchers.greaterThan
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer.DeleteForMeSync
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assert
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsSize
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SyncMessageProcessorTest_synchronizeDeleteForMe {
|
||||
|
||||
companion object {
|
||||
private val TAG = "SyncDeleteForMeTest"
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
private lateinit var messageHelper: MessageHelper
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
messageHelper = MessageHelper(harness)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
messageHelper.tearDown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleMessageDelete() {
|
||||
// GIVEN
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
messageHelper.incomingText()
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 2
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeMessage(
|
||||
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp)
|
||||
)
|
||||
|
||||
// THEN
|
||||
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleOutgoingMessageDelete() {
|
||||
// GIVEN
|
||||
val message1Timestamp = messageHelper.outgoingText().timestamp
|
||||
messageHelper.incomingText()
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 2
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeMessage(
|
||||
DeleteForMeSync(conversationId = messageHelper.alice, harness.self.id to message1Timestamp)
|
||||
)
|
||||
|
||||
// THEN
|
||||
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleGroupMessageDelete() {
|
||||
// GIVEN
|
||||
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
|
||||
messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId)
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 3
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeMessage(
|
||||
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp)
|
||||
)
|
||||
|
||||
// THEN
|
||||
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleGroupMessageDelete() {
|
||||
// GIVEN
|
||||
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
|
||||
val message3Timestamp = messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 3
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeMessage(
|
||||
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp, messageHelper.bob to message3Timestamp)
|
||||
)
|
||||
|
||||
// THEN
|
||||
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allMessagesDelete() {
|
||||
// GIVEN
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 2
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeMessage(
|
||||
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
|
||||
)
|
||||
|
||||
// THEN
|
||||
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 0
|
||||
|
||||
val threadRecord = SignalDatabase.threads.getThreadRecord(threadId)
|
||||
threadRecord assertIs null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun earlyMessagesDelete() {
|
||||
// GIVEN
|
||||
messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 1
|
||||
|
||||
// WHEN
|
||||
val nextTextMessageTimestamp = messageHelper.nextStartTime(2)
|
||||
messageHelper.syncDeleteForMeMessage(
|
||||
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to nextTextMessageTimestamp)
|
||||
)
|
||||
messageHelper.incomingText()
|
||||
|
||||
// THEN
|
||||
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
|
||||
messageCount assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleConversationMessagesDelete() {
|
||||
// GIVEN
|
||||
messageHelper.incomingText(sender = messageHelper.alice)
|
||||
val aliceMessage2 = messageHelper.incomingText(sender = messageHelper.alice).timestamp
|
||||
|
||||
messageHelper.incomingText(sender = messageHelper.bob)
|
||||
val bobMessage2 = messageHelper.incomingText(sender = messageHelper.bob).timestamp
|
||||
|
||||
val aliceThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
|
||||
aliceMessageCount assertIs 2
|
||||
|
||||
val bobThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.bob)!!
|
||||
var bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
|
||||
bobMessageCount assertIs 2
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeMessage(
|
||||
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to aliceMessage2),
|
||||
DeleteForMeSync(conversationId = messageHelper.bob, messageHelper.bob to bobMessage2)
|
||||
)
|
||||
|
||||
// THEN
|
||||
aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
|
||||
aliceMessageCount assertIs 1
|
||||
|
||||
bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
|
||||
bobMessageCount assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleConversationDelete() {
|
||||
// GIVEN
|
||||
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||
|
||||
for (i in 0 until 10) {
|
||||
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||
}
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeConversation(
|
||||
DeleteForMeSync(
|
||||
conversationId = messageHelper.alice,
|
||||
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
|
||||
isFullDelete = true
|
||||
)
|
||||
)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleConversationNoRecentsFoundDelete() {
|
||||
// GIVEN
|
||||
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||
|
||||
for (i in 0 until 10) {
|
||||
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||
}
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||
|
||||
// WHEN
|
||||
val randomFutureMessages = (1..5).map {
|
||||
messageHelper.alice to messageHelper.nextStartTime(it)
|
||||
}
|
||||
|
||||
messageHelper.syncDeleteForMeConversation(
|
||||
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, isFullDelete = true)
|
||||
)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
|
||||
|
||||
harness.inMemoryLogger.flush()
|
||||
harness.inMemoryLogger.entries().filter { it.message?.contains("Unable to find most recent received at timestamp") == true }.size assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleConversationNoRecentsFoundNonExpiringRecentsFoundDelete() {
|
||||
// GIVEN
|
||||
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||
|
||||
for (i in 0 until 10) {
|
||||
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||
}
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||
|
||||
// WHEN
|
||||
val nonExpiringMessages = messages.takeLast(5).map { it.recipientId to it.timetamp }
|
||||
|
||||
val randomFutureMessages = (1..5).map {
|
||||
messageHelper.alice to messageHelper.nextStartTime(it)
|
||||
}
|
||||
|
||||
messageHelper.syncDeleteForMeConversation(
|
||||
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, nonExpiringMessages, true)
|
||||
)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||
|
||||
harness.inMemoryLogger.flush()
|
||||
harness.inMemoryLogger.entries().filter { it.message?.contains("Using backup non-expiring messages") == true }.size assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localOnlyRemainingAfterConversationDeleteWithFullDelete() {
|
||||
// GIVEN
|
||||
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||
|
||||
Log.v(TAG, "Adding normal messages")
|
||||
for (i in 0 until 10) {
|
||||
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||
}
|
||||
|
||||
val alice = Recipient.resolved(messageHelper.alice)
|
||||
Log.v(TAG, "Adding identity message")
|
||||
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
|
||||
Log.v(TAG, "Adding profile message")
|
||||
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
|
||||
Log.v(TAG, "Adding call message")
|
||||
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
|
||||
|
||||
// WHEN
|
||||
Log.v(TAG, "Processing sync message")
|
||||
messageHelper.syncDeleteForMeConversation(
|
||||
DeleteForMeSync(
|
||||
conversationId = messageHelper.alice,
|
||||
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
|
||||
isFullDelete = true
|
||||
)
|
||||
)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun localOnlyRemainingAfterConversationDeleteWithoutFullDelete() {
|
||||
// GIVEN
|
||||
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||
|
||||
for (i in 0 until 10) {
|
||||
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||
}
|
||||
|
||||
val alice = Recipient.resolved(messageHelper.alice)
|
||||
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
|
||||
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
|
||||
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeConversation(
|
||||
DeleteForMeSync(
|
||||
conversationId = messageHelper.alice,
|
||||
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
|
||||
isFullDelete = false
|
||||
)
|
||||
)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 3
|
||||
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun groupConversationDelete() {
|
||||
// GIVEN
|
||||
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||
|
||||
for (i in 0 until 50) {
|
||||
messages += when (i % 3) {
|
||||
1 -> MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp)
|
||||
2 -> MessageTable.SyncMessageId(messageHelper.bob, messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp)
|
||||
else -> MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(messageHelper.group.recipientId).timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeConversation(
|
||||
DeleteForMeSync(
|
||||
conversationId = messageHelper.group.recipientId,
|
||||
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
|
||||
isFullDelete = true
|
||||
)
|
||||
)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleConversationDelete() {
|
||||
// GIVEN
|
||||
val allMessages = mapOf<RecipientId, MutableList<MessageTable.SyncMessageId>>(
|
||||
messageHelper.alice to mutableListOf(),
|
||||
messageHelper.bob to mutableListOf()
|
||||
)
|
||||
|
||||
allMessages.forEach { (conversation, messages) ->
|
||||
for (i in 0 until 10) {
|
||||
messages += MessageTable.SyncMessageId(conversation, messageHelper.incomingText(sender = conversation).timestamp)
|
||||
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(conversationId = conversation).timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
val threadIds = allMessages.keys.map { SignalDatabase.threads.getThreadIdFor(it)!! }
|
||||
threadIds.forEach { SignalDatabase.messages.getMessageCountForThread(it) assertIs 20 }
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeConversation(
|
||||
DeleteForMeSync(conversationId = messageHelper.alice, allMessages[messageHelper.alice]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true),
|
||||
DeleteForMeSync(conversationId = messageHelper.bob, allMessages[messageHelper.bob]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true)
|
||||
)
|
||||
|
||||
// THEN
|
||||
threadIds.forEach {
|
||||
SignalDatabase.messages.getMessageCountForThread(it) assertIs 0
|
||||
SignalDatabase.threads.getThreadRecord(it) assertIs null
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleLocalOnlyConversation() {
|
||||
// GIVEN
|
||||
val alice = Recipient.resolved(messageHelper.alice)
|
||||
|
||||
// Insert placeholder message to prevent early thread update deletes
|
||||
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
|
||||
|
||||
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
|
||||
|
||||
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
|
||||
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
|
||||
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
|
||||
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
|
||||
|
||||
// Cleanup and confirm setup
|
||||
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assert greaterThan(0)
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
|
||||
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleLocalOnlyConversation() {
|
||||
// GIVEN
|
||||
val alice = Recipient.resolved(messageHelper.alice)
|
||||
|
||||
// Insert placeholder messages in group and alice thread to prevent early thread update deletes
|
||||
val groupPlaceholderMessage = messageHelper.outgoingText(conversationId = messageHelper.group.recipientId).messageId
|
||||
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
|
||||
|
||||
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
|
||||
val groupThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.group.recipientId, isGroup = true)
|
||||
|
||||
// Identity changes
|
||||
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
|
||||
IdentityUtil.markIdentityVerified(harness.context, alice, false, true)
|
||||
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
|
||||
IdentityUtil.markIdentityVerified(harness.context, alice, false, false)
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 5
|
||||
|
||||
IdentityUtil.markIdentityUpdate(harness.context, alice.id)
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 6
|
||||
|
||||
// Calls
|
||||
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
|
||||
SignalDatabase.calls.insertOneToOneCall(2, System.currentTimeMillis(), alice.id, CallTable.Type.VIDEO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED)
|
||||
SignalDatabase.calls.insertOneToOneCall(3, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED_NOTIFICATION_PROFILE)
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 9
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(4, messageHelper.group.recipientId, CallTable.Direction.INCOMING, System.currentTimeMillis())
|
||||
SignalDatabase.calls.insertDeclinedGroupCall(5, messageHelper.group.recipientId, System.currentTimeMillis())
|
||||
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 8
|
||||
|
||||
// Detected changes
|
||||
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 10
|
||||
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 9
|
||||
|
||||
SignalDatabase.messages.insertLearnedProfileNameChangeMessage(alice, null, "username.42")
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 11
|
||||
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 9
|
||||
|
||||
SignalDatabase.messages.insertNumberChangeMessages(alice.id)
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 12
|
||||
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
|
||||
|
||||
SignalDatabase.messages.insertSmsExportMessage(alice.id, SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!)
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 13
|
||||
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
|
||||
|
||||
SignalDatabase.messages.insertSessionSwitchoverEvent(alice.id, aliceThreadId, SessionSwitchoverEvent())
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 14
|
||||
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
|
||||
|
||||
// Sent failed
|
||||
SignalDatabase.messages.markAsSending(messageHelper.outgoingText().messageId)
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 15
|
||||
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 16
|
||||
messageHelper.outgoingText().let {
|
||||
SignalDatabase.messages.markAsSending(it.messageId)
|
||||
SignalDatabase.messages.markAsRateLimited(it.messageId)
|
||||
}
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 17
|
||||
|
||||
// Group change
|
||||
messageHelper.outgoingGroupChange()
|
||||
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 11
|
||||
|
||||
// Cleanup and confirm setup
|
||||
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
|
||||
SignalDatabase.messages.deleteMessage(messageId = groupPlaceholderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
|
||||
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 16
|
||||
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
|
||||
}
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice, messageHelper.group.recipientId)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
|
||||
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 0
|
||||
SignalDatabase.threads.getThreadRecord(groupThreadId) assertIs null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleLocalOnlyConversationHasAddressable() {
|
||||
// GIVEN
|
||||
val messages = mutableListOf<MessageTable.SyncMessageId>()
|
||||
|
||||
for (i in 0 until 10) {
|
||||
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
|
||||
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
|
||||
}
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
|
||||
|
||||
// THEN
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
|
||||
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
|
||||
|
||||
harness.inMemoryLogger.flush()
|
||||
harness.inMemoryLogger.entries().filter { it.message?.contains("Thread is not local only") == true }.size assertIs 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun singleAttachmentDeletes() {
|
||||
// GIVEN
|
||||
val message1 = messageHelper.outgoingText { message ->
|
||||
message.copy(
|
||||
attachments = listOf(
|
||||
messageHelper.outgoingAttachment(byteArrayOf(1, 2, 3)),
|
||||
messageHelper.outgoingAttachment(byteArrayOf(2, 3, 4), null),
|
||||
messageHelper.outgoingAttachment(byteArrayOf(5, 6, 7), null),
|
||||
messageHelper.outgoingAttachment(byteArrayOf(10, 11, 12))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
var attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
attachments assertIsSize 4
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
|
||||
// Has all three
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[0].attachmentId,
|
||||
attachment = attachments[0].copy(digest = byteArrayOf(attachments[0].attachmentId.id.toByte())),
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
|
||||
// Missing uuid and digest
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[1].attachmentId,
|
||||
attachment = attachments[1],
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
|
||||
// Missing uuid and plain text
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[2].attachmentId,
|
||||
attachment = attachments[2].copy(digest = byteArrayOf(attachments[2].attachmentId.id.toByte())),
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
SignalDatabase.rawDatabase.update(AttachmentTable.TABLE_NAME).values(AttachmentTable.DATA_HASH_END to null).where("${AttachmentTable.ID} = ?", attachments[2].attachmentId).run()
|
||||
|
||||
// Different has all three
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
|
||||
id = attachments[3].attachmentId,
|
||||
attachment = attachments[3].copy(digest = byteArrayOf(attachments[3].attachmentId.id.toByte())),
|
||||
uploadTimestamp = message1.timestamp + 1
|
||||
)
|
||||
|
||||
attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
|
||||
// WHEN
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[0].uuid,
|
||||
attachments[0].remoteDigest,
|
||||
attachments[0].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
var updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 3
|
||||
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[0].attachmentId }
|
||||
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[1].uuid,
|
||||
attachments[1].remoteDigest,
|
||||
attachments[1].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 2
|
||||
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[1].attachmentId }
|
||||
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[2].uuid,
|
||||
attachments[2].remoteDigest,
|
||||
attachments[2].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
|
||||
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 1
|
||||
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[2].attachmentId }
|
||||
|
||||
messageHelper.syncDeleteForMeAttachment(
|
||||
conversationId = messageHelper.alice,
|
||||
message = message1.author to message1.timestamp,
|
||||
attachments[3].uuid,
|
||||
attachments[3].remoteDigest,
|
||||
attachments[3].dataHash
|
||||
)
|
||||
|
||||
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
|
||||
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
|
||||
updatedAttachments assertIsSize 0
|
||||
|
||||
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.copy(
|
||||
uuid: UUID? = this.uuid,
|
||||
digest: ByteArray? = this.remoteDigest
|
||||
): Attachment {
|
||||
return DatabaseAttachment(
|
||||
attachmentId = this.attachmentId,
|
||||
mmsId = this.mmsId,
|
||||
hasData = this.hasData,
|
||||
hasThumbnail = false,
|
||||
hasArchiveThumbnail = false,
|
||||
contentType = this.contentType,
|
||||
transferProgress = this.transferState,
|
||||
size = this.size,
|
||||
fileName = this.fileName,
|
||||
cdn = this.cdn,
|
||||
location = this.remoteLocation,
|
||||
key = this.remoteKey,
|
||||
digest = digest,
|
||||
incrementalDigest = this.incrementalDigest,
|
||||
incrementalMacChunkSize = this.incrementalMacChunkSize,
|
||||
fastPreflightId = this.fastPreflightId,
|
||||
voiceNote = this.voiceNote,
|
||||
borderless = this.borderless,
|
||||
videoGif = this.videoGif,
|
||||
width = this.width,
|
||||
height = this.height,
|
||||
quote = this.quote,
|
||||
caption = this.caption,
|
||||
stickerLocator = this.stickerLocator,
|
||||
blurHash = this.blurHash,
|
||||
audioHash = this.audioHash,
|
||||
transformProperties = this.transformProperties,
|
||||
displayOrder = this.displayOrder,
|
||||
uploadTimestamp = this.uploadTimestamp,
|
||||
dataHash = this.dataHash,
|
||||
archiveCdn = this.archiveCdn,
|
||||
archiveThumbnailCdn = this.archiveThumbnailCdn,
|
||||
archiveMediaName = this.archiveMediaName,
|
||||
archiveMediaId = this.archiveMediaId,
|
||||
thumbnailRestoreState = this.thumbnailRestoreState,
|
||||
uuid = uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.migrations
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.count
|
||||
import org.signal.core.util.readToSingleInt
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.util.Currency
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SubscriberIdMigrationJobTest {
|
||||
|
||||
private val testSubject = SubscriberIdMigrationJob()
|
||||
|
||||
@Test
|
||||
fun givenNoSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectNoDatabaseEntries() {
|
||||
testSubject.run()
|
||||
|
||||
val actual = SignalDatabase.inAppPaymentSubscribers.readableDatabase.count()
|
||||
.from(InAppPaymentSubscriberTable.TABLE_NAME)
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
|
||||
actual assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenUSDSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectASingleEntry() {
|
||||
val subscriberId = SubscriberId.generate()
|
||||
SignalStore.donations.setSubscriberCurrency(Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
SignalStore.donations.setSubscriber("USD", subscriberId)
|
||||
SignalStore.donations.setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
|
||||
SignalStore.donations.shouldCancelSubscriptionBeforeNextSubscribeAttempt = true
|
||||
|
||||
testSubject.run()
|
||||
|
||||
val actual = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode("USD", InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
|
||||
actual.assertIsNotNull()
|
||||
actual!!.subscriberId.bytes assertIs subscriberId.bytes
|
||||
actual.paymentMethodType assertIs InAppPaymentData.PaymentMethodType.PAYPAL
|
||||
actual.requiresCancel assertIs true
|
||||
actual.currency assertIs Currency.getInstance("USD")
|
||||
actual.type assertIs InAppPaymentSubscriberRecord.Type.DONATION
|
||||
}
|
||||
}
|
||||
@@ -90,10 +90,10 @@ class SafetyNumberBottomSheetRepositoryTest {
|
||||
subjectUnderTest.removeFromStories(toRemove, listOf(destinationKey)).subscribe()
|
||||
testSubscriber.request(1)
|
||||
testScheduler.triggerActions()
|
||||
testSubscriber.awaitCount(3)
|
||||
testSubscriber.awaitCount(2)
|
||||
|
||||
// THEN
|
||||
testSubscriber.assertValueAt(2) { map ->
|
||||
testSubscriber.assertValueAt(1) { map ->
|
||||
assertMatch(
|
||||
map,
|
||||
mapOf(
|
||||
@@ -116,10 +116,10 @@ class SafetyNumberBottomSheetRepositoryTest {
|
||||
subjectUnderTest.removeAllFromStory(distributionListMembers, distributionList).subscribe()
|
||||
testSubscriber.request(1)
|
||||
testScheduler.triggerActions()
|
||||
testSubscriber.awaitCount(3)
|
||||
testSubscriber.awaitCount(2)
|
||||
|
||||
// THEN
|
||||
testSubscriber.assertValueAt(2) { map ->
|
||||
testSubscriber.assertValueAt(1) { map ->
|
||||
assertMatch(map, mapOf())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ class ContactRecordProcessorTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
SignalStore.account().setE164(E164_SELF)
|
||||
SignalStore.account().setAci(ACI_SELF)
|
||||
SignalStore.account().setPni(PNI_SELF)
|
||||
SignalStore.account.setE164(E164_SELF)
|
||||
SignalStore.account.setAci(ACI_SELF)
|
||||
SignalStore.account.setPni(PNI_SELF)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -4,7 +4,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -30,14 +30,14 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
|
||||
uuid = serviceId.rawUuid,
|
||||
e164 = e164,
|
||||
deviceId = 1,
|
||||
identityKey = SignalStore.account().aciIdentityKey.publicKey.publicKey,
|
||||
identityKey = SignalStore.account.aciIdentityKey.publicKey.publicKey,
|
||||
expires = 31337
|
||||
)
|
||||
|
||||
fun process(envelope: Envelope, serverDeliveredTimestamp: Long) {
|
||||
val start = System.currentTimeMillis()
|
||||
val bufferedStore = BufferedProtocolStore.create()
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
AppDependencies.incomingMessageObserver
|
||||
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
|
||||
?.mapNotNull { it.run() }
|
||||
?.forEach { it.enqueue() }
|
||||
@@ -48,7 +48,7 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
|
||||
}
|
||||
|
||||
fun encrypt(now: Long, destination: Recipient): Envelope {
|
||||
return ApplicationDependencies.getSignalServiceMessageSender().getEncryptedMessage(
|
||||
return AppDependencies.signalServiceMessageSender.getEncryptedMessage(
|
||||
SignalServiceAddress(destination.requireServiceId(), destination.requireE164()),
|
||||
FakeClientHelpers.getTargetUnidentifiedAccess(ProfileKeyUtil.getSelfProfileKey(), ProfileKey(destination.profileKey), aliceSenderCertificate),
|
||||
1,
|
||||
|
||||
@@ -80,7 +80,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
}
|
||||
|
||||
private fun getAliceServiceId(): ServiceId {
|
||||
return SignalStore.account().requireAci()
|
||||
return SignalStore.account.requireAci()
|
||||
}
|
||||
|
||||
private fun getAlicePreKeyBundle(): PreKeyBundle {
|
||||
@@ -103,7 +103,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
val selfSignedPreKeyRecord = SignalDatabase.signedPreKeys.get(getAliceServiceId(), selfSignedPreKeyId)!!
|
||||
|
||||
return PreKeyBundle(
|
||||
SignalStore.account().registrationId,
|
||||
SignalStore.account.registrationId,
|
||||
1,
|
||||
selfPreKeyId,
|
||||
selfPreKeyRecord.keyPair.publicKey,
|
||||
@@ -115,11 +115,11 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
}
|
||||
|
||||
private fun getAliceProtocolAddress(): SignalProtocolAddress {
|
||||
return SignalProtocolAddress(SignalStore.account().requireAci().toString(), 1)
|
||||
return SignalProtocolAddress(SignalStore.account.requireAci().toString(), 1)
|
||||
}
|
||||
|
||||
private fun getAlicePublicKey(): IdentityKey {
|
||||
return SignalStore.account().aciIdentityKey.publicKey
|
||||
return SignalStore.account.aciIdentityKey.publicKey
|
||||
}
|
||||
|
||||
private fun getAliceProfileKey(): ProfileKey {
|
||||
@@ -139,10 +139,12 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean = true
|
||||
override fun loadSession(address: SignalProtocolAddress?): SessionRecord = aliceSessionRecord ?: SessionRecord()
|
||||
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): Boolean = false
|
||||
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) { aliceSessionRecord = record }
|
||||
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) {
|
||||
aliceSessionRecord = record
|
||||
}
|
||||
override fun getSubDeviceSessions(name: String?): List<Int> = emptyList()
|
||||
override fun containsSession(address: SignalProtocolAddress?): Boolean = aliceSessionRecord != null
|
||||
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account().aciIdentityKey.publicKey
|
||||
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account.aciIdentityKey.publicKey
|
||||
override fun loadPreKey(preKeyId: Int): PreKeyRecord = throw UnsupportedOperationException()
|
||||
override fun storePreKey(preKeyId: Int, record: PreKeyRecord?) = throw UnsupportedOperationException()
|
||||
override fun containsPreKey(preKeyId: Int): Boolean = throw UnsupportedOperationException()
|
||||
|
||||
@@ -2,12 +2,15 @@ package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.thoughtcrime.securesms.messages.TestMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
@@ -61,13 +64,13 @@ object MessageContentFuzzer {
|
||||
* - An expire timer value
|
||||
* - Bold style body ranges
|
||||
*/
|
||||
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
|
||||
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null, allowExpireTimeChanges: Boolean = true): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
timestamp = sentTimestamp
|
||||
body = string()
|
||||
if (random.nextBoolean()) {
|
||||
if (allowExpireTimeChanges && random.nextBoolean()) {
|
||||
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
|
||||
}
|
||||
if (random.nextBoolean()) {
|
||||
@@ -150,6 +153,121 @@ object MessageContentFuzzer {
|
||||
).build()
|
||||
}
|
||||
|
||||
fun syncDeleteForMeMessage(allDeletes: List<DeleteForMeSync>): Content {
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage(
|
||||
deleteForMe = SyncMessage.DeleteForMe(
|
||||
messageDeletes = allDeletes.map { (conversationId, conversationDeletes) ->
|
||||
val conversation = Recipient.resolved(conversationId)
|
||||
SyncMessage.DeleteForMe.MessageDeletes(
|
||||
conversation = if (conversation.isGroup) {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||
} else {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||
},
|
||||
|
||||
messages = conversationDeletes.map { (author, timestamp) ->
|
||||
SyncMessage.DeleteForMe.AddressableMessage(
|
||||
authorServiceId = Recipient.resolved(author).requireAci().toString(),
|
||||
sentTimestamp = timestamp
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
).build()
|
||||
}
|
||||
|
||||
fun syncDeleteForMeConversation(allDeletes: List<DeleteForMeSync>): Content {
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage(
|
||||
deleteForMe = SyncMessage.DeleteForMe(
|
||||
conversationDeletes = allDeletes.map { delete ->
|
||||
val conversation = Recipient.resolved(delete.conversationId)
|
||||
SyncMessage.DeleteForMe.ConversationDelete(
|
||||
conversation = if (conversation.isGroup) {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||
} else {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||
},
|
||||
|
||||
mostRecentMessages = delete.messages.map { (author, timestamp) ->
|
||||
SyncMessage.DeleteForMe.AddressableMessage(
|
||||
authorServiceId = Recipient.resolved(author).requireAci().toString(),
|
||||
sentTimestamp = timestamp
|
||||
)
|
||||
},
|
||||
|
||||
mostRecentNonExpiringMessages = delete.nonExpiringMessages.map { (author, timestamp) ->
|
||||
SyncMessage.DeleteForMe.AddressableMessage(
|
||||
authorServiceId = Recipient.resolved(author).requireAci().toString(),
|
||||
sentTimestamp = timestamp
|
||||
)
|
||||
},
|
||||
|
||||
isFullDelete = delete.isFullDelete
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
).build()
|
||||
}
|
||||
|
||||
fun syncDeleteForMeLocalOnlyConversation(conversations: List<RecipientId>): Content {
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage(
|
||||
deleteForMe = SyncMessage.DeleteForMe(
|
||||
localOnlyConversationDeletes = conversations.map { conversationId ->
|
||||
val conversation = Recipient.resolved(conversationId)
|
||||
SyncMessage.DeleteForMe.LocalOnlyConversationDelete(
|
||||
conversation = if (conversation.isGroup) {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||
} else {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
).build()
|
||||
}
|
||||
|
||||
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): Content {
|
||||
val conversation = Recipient.resolved(conversationId)
|
||||
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage(
|
||||
deleteForMe = SyncMessage.DeleteForMe(
|
||||
attachmentDeletes = listOf(
|
||||
SyncMessage.DeleteForMe.AttachmentDelete(
|
||||
conversation = if (conversation.isGroup) {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
|
||||
} else {
|
||||
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
|
||||
},
|
||||
targetMessage = SyncMessage.DeleteForMe.AddressableMessage(
|
||||
authorServiceId = Recipient.resolved(message.first).requireAci().toString(),
|
||||
sentTimestamp = message.second
|
||||
),
|
||||
uuid = uuid?.let { UuidUtil.toByteString(it) },
|
||||
fallbackDigest = digest?.toByteString(),
|
||||
fallbackPlaintextHash = plainTextHash?.let { Base64.decodeOrNull(it)?.toByteString() }
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that may be:
|
||||
* - A text body
|
||||
@@ -278,7 +396,7 @@ object MessageContentFuzzer {
|
||||
caption = string(allowNullString = true)
|
||||
blurHash = string()
|
||||
uploadTimestamp = random.nextLong()
|
||||
cdnNumber = 1
|
||||
cdnNumber = 2
|
||||
|
||||
build()
|
||||
}
|
||||
@@ -290,4 +408,14 @@ object MessageContentFuzzer {
|
||||
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
|
||||
return envelopeTimestamp + 10
|
||||
}
|
||||
|
||||
data class DeleteForMeSync(
|
||||
val conversationId: RecipientId,
|
||||
val messages: List<Pair<RecipientId, Long>>,
|
||||
val nonExpiringMessages: List<Pair<RecipientId, Long>> = emptyList(),
|
||||
val isFullDelete: Boolean = true,
|
||||
val attachments: List<Pair<Long, AttachmentTable.SyncAttachmentId>> = emptyList()
|
||||
) {
|
||||
constructor(conversationId: RecipientId, vararg messages: Pair<RecipientId, Long>) : this(conversationId, messages.toList())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ object MockProvider {
|
||||
|
||||
val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply {
|
||||
svr1Credentials = AuthCredentials.create("username", "password")
|
||||
svr2Credentials = null
|
||||
svr2Credentials = AuthCredentials.create("username", "password")
|
||||
}
|
||||
|
||||
val primaryOnlyDeviceList = DeviceInfoList().apply {
|
||||
@@ -68,7 +68,7 @@ object MockProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account().aciIdentityKey, deviceId: Int): PreKeyResponse {
|
||||
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account.aciIdentityKey, deviceId: Int): PreKeyResponse {
|
||||
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
|
||||
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())
|
||||
|
||||
|
||||
@@ -55,5 +55,5 @@ inline fun <reified T> RecordedRequest.parsedRequestBody(): T {
|
||||
}
|
||||
|
||||
private fun defaultRequestPredicate(verb: String, path: String, predicate: RequestPredicate = { true }): RequestPredicate = { request ->
|
||||
request.method == verb && request.path.startsWith("/$path") && predicate(request)
|
||||
request.method == verb && request.path?.startsWith("/$path") == true && predicate(request)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ 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.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
@@ -47,7 +47,7 @@ import java.util.UUID
|
||||
*/
|
||||
class SignalActivityRule(private val othersCount: Int = 4, private val createGroup: Boolean = false) : ExternalResource() {
|
||||
|
||||
val application: Application = ApplicationDependencies.getApplication()
|
||||
val application: Application = AppDependencies.application
|
||||
|
||||
lateinit var context: Context
|
||||
private set
|
||||
@@ -90,8 +90,8 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
||||
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
||||
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
||||
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||
|
||||
val registrationRepository = RegistrationRepository(application)
|
||||
|
||||
@@ -111,19 +111,19 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
||||
verifyAccountResponse = VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
|
||||
masterKey = null,
|
||||
pin = null,
|
||||
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
|
||||
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
|
||||
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
|
||||
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
|
||||
),
|
||||
false
|
||||
).blockingGet()
|
||||
|
||||
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
||||
|
||||
SignalStore.svr().optOut()
|
||||
SignalStore.svr.optOut()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||
|
||||
SignalStore.settings().isMessageNotificationsEnabled = false
|
||||
SignalStore.settings.isMessageNotificationsEnabled = false
|
||||
|
||||
return Recipient.self()
|
||||
}
|
||||
@@ -141,11 +141,11 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
||||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, false))
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
||||
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
||||
others += recipientId
|
||||
othersKeys += otherIdentity
|
||||
}
|
||||
@@ -158,14 +158,14 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
||||
}
|
||||
|
||||
fun changeIdentityKey(recipient: Recipient, identityKey: IdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey) {
|
||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
|
||||
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
|
||||
}
|
||||
|
||||
fun getIdentity(recipient: Recipient): IdentityKey {
|
||||
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
|
||||
return AppDependencies.protocolStore.aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
|
||||
}
|
||||
|
||||
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
|
||||
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
|
||||
AppDependencies.protocolStore.aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ class SignalDatabaseRule(
|
||||
override fun starting(description: Description?) {
|
||||
deleteAllThreads()
|
||||
|
||||
SignalStore.account().setAci(localAci)
|
||||
SignalStore.account().setPni(localPni)
|
||||
SignalStore.account.setAci(localAci)
|
||||
SignalStore.account.setPni(localPni)
|
||||
}
|
||||
|
||||
override fun finished(description: Description?) {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import android.database.Cursor
|
||||
import android.util.Base64
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.MatcherAssert.assertThat
|
||||
import org.hamcrest.Matchers.hasSize
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.hamcrest.Matchers.not
|
||||
import org.hamcrest.Matchers.notNullValue
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.select
|
||||
@@ -56,33 +57,41 @@ infix fun <E, T : Collection<E>> T.assertIsSize(expected: Int) {
|
||||
assertThat(this, hasSize(expected))
|
||||
}
|
||||
|
||||
infix fun <T : Any> T.assert(matcher: Matcher<T>) {
|
||||
assertThat(this, matcher)
|
||||
}
|
||||
|
||||
fun CountDownLatch.awaitFor(duration: Duration) {
|
||||
if (!await(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS)) {
|
||||
throw TimeoutException("Latch await took longer than ${duration.inWholeMilliseconds}ms")
|
||||
}
|
||||
}
|
||||
|
||||
fun dumpTableToLogs(tag: String = "TestUtils", table: String) {
|
||||
dumpTable(table).forEach { Log.d(tag, it.toString()) }
|
||||
fun dumpTableToLogs(tag: String = "TestUtils", table: String, columns: Set<String>? = null) {
|
||||
dumpTable(table, columns).forEach { Log.d(tag, it.toString()) }
|
||||
}
|
||||
|
||||
fun dumpTable(table: String): List<List<Pair<String, String?>>> {
|
||||
fun dumpTable(table: String, columns: Set<String>?): List<List<Pair<String, String?>>> {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select()
|
||||
.from(table)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
|
||||
val index = cursor.getColumnIndex(column)
|
||||
var data: String? = when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
val map: List<Pair<String, String?>> = cursor.columnNames.mapNotNull { column ->
|
||||
if (columns == null || columns.contains(column)) {
|
||||
val index = cursor.getColumnIndex(column)
|
||||
var data: String? = when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_BLOB -> Hex.toStringCondensed(cursor.getBlob(index))
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
|
||||
column to data
|
||||
column to data
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
/**
|
||||
* A class that allows us to inject feature flags during tests.
|
||||
*/
|
||||
public final class FeatureFlagsAccessor {
|
||||
|
||||
public static void forceValue(String key, Object value) {
|
||||
FeatureFlags.FORCED_VALUES.put(key, value);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ package org.signal.benchmark
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.account.PreKeyUpload
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
@@ -16,15 +16,15 @@ import java.util.Optional
|
||||
class DummyAccountManagerFactory : AccountManagerFactory() {
|
||||
override fun createAuthenticated(context: Context, aci: ACI, pni: PNI, number: String, deviceId: Int, password: String): SignalServiceAccountManager {
|
||||
return DummyAccountManager(
|
||||
ApplicationDependencies.getSignalServiceNetworkAccess().getConfiguration(number),
|
||||
AppDependencies.signalServiceNetworkAccess.getConfiguration(number),
|
||||
aci,
|
||||
pni,
|
||||
number,
|
||||
deviceId,
|
||||
password,
|
||||
BuildConfig.SIGNAL_AGENT,
|
||||
FeatureFlags.okHttpAutomaticRetry(),
|
||||
FeatureFlags.groupLimits().hardLimit
|
||||
RemoteConfig.okHttpAutomaticRetry,
|
||||
RemoteConfig.groupLimits.hardLimit
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,8 @@ object TestMessages {
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
System.currentTimeMillis()
|
||||
System.currentTimeMillis(),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -184,7 +185,8 @@ object TestMessages {
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
System.currentTimeMillis()
|
||||
System.currentTimeMillis(),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
@@ -35,7 +35,7 @@ object TestUsers {
|
||||
private var generatedOthers: Int = 0
|
||||
|
||||
fun setupSelf(): Recipient {
|
||||
val application: Application = ApplicationDependencies.getApplication()
|
||||
val application: Application = AppDependencies.application
|
||||
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
|
||||
|
||||
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
|
||||
@@ -44,8 +44,8 @@ object TestUsers {
|
||||
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
||||
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
||||
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
SignalStore.account.generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account.generatePniIdentityKeyIfNecessary()
|
||||
|
||||
val registrationRepository = RegistrationRepository(application)
|
||||
val registrationData = RegistrationData(
|
||||
@@ -63,8 +63,8 @@ object TestUsers {
|
||||
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
|
||||
masterKey = null,
|
||||
pin = null,
|
||||
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
|
||||
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
|
||||
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
|
||||
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
|
||||
)
|
||||
|
||||
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
|
||||
@@ -77,7 +77,7 @@ object TestUsers {
|
||||
|
||||
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
||||
|
||||
SignalStore.svr().optOut()
|
||||
SignalStore.svr.optOut()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||
|
||||
@@ -100,11 +100,11 @@ object TestUsers {
|
||||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
||||
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
|
||||
|
||||
others += recipientId
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
@@ -123,7 +123,7 @@ class ConversationElementGenerator {
|
||||
)
|
||||
|
||||
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
|
||||
ApplicationDependencies.getApplication(),
|
||||
AppDependencies.application,
|
||||
record,
|
||||
Recipient.UNKNOWN
|
||||
)
|
||||
|
||||
@@ -280,7 +280,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
|
||||
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -304,6 +304,10 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onPaymentTombstoneClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onShowSafetyTips(forGroup: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -748,13 +748,6 @@
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".backup.v2.ui.subscription.MessageBackupsFlowActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.settings.StorySettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@@ -797,13 +790,6 @@
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".badges.gifts.flow.GiftFlowActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".wallpaper.ChatWallpaperActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@@ -830,14 +816,7 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".registration.RegistrationNavigationActivity"
|
||||
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=".registration.v2.ui.RegistrationV2Activity"
|
||||
<activity android:name=".registration.ui.RegistrationActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
@@ -982,8 +961,8 @@
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".backup.v2.ui.subscription.MessageBackupsTestRestoreActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
<activity android:name=".registration.ui.restore.RemoteRestoreActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".profiles.manage.EditProfileActivity"
|
||||
@@ -1099,7 +1078,7 @@
|
||||
android:theme="@style/Theme.Signal.WallpaperCropper"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrImageSelectionActivity"
|
||||
<activity android:name=".components.settings.app.usernamelinks.main.QrImageSelectionActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:exported="false"/>
|
||||
@@ -1114,10 +1093,10 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
<activity android:name=".components.settings.app.subscription.donate.CheckoutFlowActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:enabled="true"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
|
||||
object AppCapabilities {
|
||||
@@ -17,7 +18,8 @@ object AppCapabilities {
|
||||
stories = true,
|
||||
giftBadges = true,
|
||||
pni = true,
|
||||
paymentActivation = true
|
||||
paymentActivation = true,
|
||||
deleteSync = RemoteConfig.deleteSyncEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
@@ -37,29 +37,29 @@ public final class AppInitialization {
|
||||
TextSecurePreferences.setReadReceiptsEnabled(context, true);
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
|
||||
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
}
|
||||
|
||||
public static void onPostBackupRestore(@NonNull Context context) {
|
||||
Log.i(TAG, "onPostBackupRestore()");
|
||||
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.onPostBackupRestore();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
SignalStore.onboarding().clearAll();
|
||||
TextSecurePreferences.onPostBackupRestore(context);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
EmojiSearchIndexDownloadJob.scheduleImmediately();
|
||||
}
|
||||
|
||||
@@ -74,12 +74,12 @@ public final class AppInitialization {
|
||||
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
|
||||
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
|
||||
TextSecurePreferences.setPasswordDisabled(context, true);
|
||||
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
|
||||
SignalStore.onFirstEverAppLaunch();
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
|
||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,20 +47,22 @@ import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
|
||||
import org.thoughtcrime.securesms.database.LogDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.gcm.FcmFetchManager;
|
||||
import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
|
||||
import org.thoughtcrime.securesms.jobs.BuildExpirationConfirmationJob;
|
||||
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.ExternalLaunchDonationJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentAuthCheckJob;
|
||||
import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
|
||||
@@ -70,7 +72,6 @@ import org.thoughtcrime.securesms.jobs.RefreshSvrCredentialsJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
|
||||
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
@@ -97,7 +98,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -143,7 +144,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
if (FeatureFlags.internalUser()) {
|
||||
if (RemoteConfig.internalUser()) {
|
||||
Tracer.getInstance().setMaxBufferSize(35_000);
|
||||
}
|
||||
|
||||
@@ -168,7 +169,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addBlocking("scrubber", () -> Scrubber.setIdentifierHmacKeyProvider(() -> SignalStore.svr().getOrCreateMasterKey().deriveLoggingKey()))
|
||||
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
|
||||
.addBlocking("app-migrations", this::initializeApplicationMigrations)
|
||||
.addBlocking("lifecycle-observer", () -> ApplicationDependencies.getAppForegroundObserver().addListener(this))
|
||||
.addBlocking("lifecycle-observer", () -> AppDependencies.getAppForegroundObserver().addListener(this))
|
||||
.addBlocking("message-retriever", this::initializeMessageRetrieval)
|
||||
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
|
||||
.addBlocking("proxy-init", () -> {
|
||||
@@ -178,7 +179,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
})
|
||||
.addBlocking("blob-provider", this::initializeBlobProvider)
|
||||
.addBlocking("feature-flags", FeatureFlags::init)
|
||||
.addBlocking("remote-config", RemoteConfig::init)
|
||||
.addBlocking("ring-rtc", this::initializeRingRtc)
|
||||
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
|
||||
.addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete())
|
||||
@@ -196,27 +197,27 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||
.addNonBlocking(this::beginJobLoop)
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addNonBlocking(() -> AppDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addNonBlocking(this::ensureProfileUploaded)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
|
||||
.addNonBlocking(() -> AppDependencies.getExpireStoriesManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> AppDependencies.getDeletedCallEventManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(this::initializeTrimThreadsByDateManager)
|
||||
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), RemoteConfig.retryRespondMaxAge()))
|
||||
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
|
||||
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
|
||||
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
|
||||
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
|
||||
.addPostRender(() -> AppDependencies.getJobManager().add(new FontDownloaderJob()))
|
||||
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
|
||||
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
|
||||
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
|
||||
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
|
||||
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
|
||||
.addPostRender(() -> AppDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
|
||||
.addPostRender(() -> AppDependencies.getRecipientCache().warmUp())
|
||||
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupRingCleanupJob::enqueue)
|
||||
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
|
||||
@@ -233,20 +234,20 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
long startTime = System.currentTimeMillis();
|
||||
Log.i(TAG, "App is now visible.");
|
||||
|
||||
ApplicationDependencies.getFrameRateTracker().start();
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
ApplicationDependencies.getDeadlockDetector().start();
|
||||
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
|
||||
ExternalLaunchDonationJob.enqueueIfNecessary();
|
||||
AppDependencies.getFrameRateTracker().start();
|
||||
AppDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
AppDependencies.getDeadlockDetector().start();
|
||||
InAppPaymentKeepAliveJob.enqueueAndTrackTimeIfNecessary();
|
||||
FcmFetchManager.onForeground(this);
|
||||
startAnrDetector();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
InAppPaymentAuthCheckJob.enqueueIfNeeded();
|
||||
RemoteConfig.refreshIfNecessary();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary();
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getShakeToReport().enable();
|
||||
AppDependencies.getShakeToReport().enable();
|
||||
checkBuildExpiration();
|
||||
MemoryTracker.start();
|
||||
|
||||
@@ -255,7 +256,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
long timeDiff = currentTime - lastForegroundTime;
|
||||
|
||||
if (timeDiff < 0) {
|
||||
Log.w(TAG, "Time travel! The system clock has moved backwards. (currentTime: " + currentTime + " ms, lastForegroundTime: " + lastForegroundTime + " ms, diff: " + timeDiff + " ms)");
|
||||
Log.w(TAG, "Time travel! The system clock has moved backwards. (currentTime: " + currentTime + " ms, lastForegroundTime: " + lastForegroundTime + " ms, diff: " + timeDiff + " ms)", true);
|
||||
}
|
||||
|
||||
SignalStore.misc().setLastForegroundTime(currentTime);
|
||||
@@ -268,18 +269,18 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
public void onBackground() {
|
||||
Log.i(TAG, "App is no longer visible.");
|
||||
KeyCachingService.onAppBackgrounded(this);
|
||||
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
|
||||
ApplicationDependencies.getFrameRateTracker().stop();
|
||||
ApplicationDependencies.getShakeToReport().disable();
|
||||
ApplicationDependencies.getDeadlockDetector().stop();
|
||||
AppDependencies.getMessageNotifier().clearVisibleThread();
|
||||
AppDependencies.getFrameRateTracker().stop();
|
||||
AppDependencies.getShakeToReport().disable();
|
||||
AppDependencies.getDeadlockDetector().stop();
|
||||
MemoryTracker.stop();
|
||||
AnrDetector.stop();
|
||||
}
|
||||
|
||||
public void checkBuildExpiration() {
|
||||
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||
Log.w(TAG, "Build expired!");
|
||||
SignalStore.misc().setClientDeprecated(true);
|
||||
if (Util.getTimeUntilBuildExpiry(SignalStore.misc().getEstimatedServerTime()) <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||
Log.w(TAG, "Build potentially expired! Enqueing job to check.", true);
|
||||
AppDependencies.getJobManager().add(new BuildExpirationConfirmationJob());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +289,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
* This is so we can capture ANR's that happen on boot before the foreground event.
|
||||
*/
|
||||
private void startAnrDetector() {
|
||||
AnrDetector.start(TimeUnit.SECONDS.toMillis(5), FeatureFlags::internalUser, (dumps) -> {
|
||||
AnrDetector.start(TimeUnit.SECONDS.toMillis(5), RemoteConfig::internalUser, (dumps) -> {
|
||||
LogDatabase.getInstance(this).anrs().save(System.currentTimeMillis(), dumps);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
@@ -313,9 +314,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
@VisibleForTesting
|
||||
protected void initializeLogging() {
|
||||
Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), new PersistentLogger(this));
|
||||
Log.initialize(RemoteConfig::internalUser, new AndroidLogger(), new PersistentLogger(this));
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
||||
SignalProtocolLoggerProvider.initializeLogging(BuildConfig.LIBSIGNAL_LOG_LEVEL);
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
Log.blockUntilAllWritesFinished();
|
||||
@@ -354,16 +356,16 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
|
||||
private void initializeApplicationMigrations() {
|
||||
ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager());
|
||||
ApplicationMigrations.onApplicationCreate(this, AppDependencies.getJobManager());
|
||||
}
|
||||
|
||||
public void initializeMessageRetrieval() {
|
||||
ApplicationDependencies.getIncomingMessageObserver();
|
||||
AppDependencies.getIncomingMessageObserver();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void initializeAppDependencies() {
|
||||
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this));
|
||||
AppDependencies.init(this, new ApplicationDependencyProvider(this));
|
||||
}
|
||||
|
||||
private void initializeFirstEverAppLaunch() {
|
||||
@@ -386,34 +388,36 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
private void initializeFcmCheck() {
|
||||
if (SignalStore.account().isRegistered()) {
|
||||
long nextSetTime = SignalStore.account().getFcmTokenLastSetTime() + TimeUnit.HOURS.toMillis(6);
|
||||
long lastSetTime = SignalStore.account().getFcmTokenLastSetTime();
|
||||
long nextSetTime = lastSetTime + TimeUnit.HOURS.toMillis(6);
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (SignalStore.account().getFcmToken() == null || nextSetTime <= System.currentTimeMillis()) {
|
||||
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
|
||||
if (SignalStore.account().getFcmToken() == null || nextSetTime <= now || lastSetTime > now) {
|
||||
AppDependencies.getJobManager().add(new FcmRefreshJob());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeExpiringMessageManager() {
|
||||
ApplicationDependencies.getExpiringMessageManager().checkSchedule();
|
||||
AppDependencies.getExpiringMessageManager().checkSchedule();
|
||||
}
|
||||
|
||||
private void initializeRevealableMessageManager() {
|
||||
ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
|
||||
AppDependencies.getViewOnceMessageManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializePendingRetryReceiptManager() {
|
||||
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
AppDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializeScheduledMessageManager() {
|
||||
ApplicationDependencies.getScheduledMessageManager().scheduleIfNecessary();
|
||||
AppDependencies.getScheduledMessageManager().scheduleIfNecessary();
|
||||
}
|
||||
|
||||
private void initializeTrimThreadsByDateManager() {
|
||||
KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration();
|
||||
if (keepMessagesDuration != KeepMessagesDuration.FOREVER) {
|
||||
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
|
||||
AppDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,10 +438,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
private void initializeRingRtc() {
|
||||
try {
|
||||
Map<String, String> fieldTrials = new HashMap<>();
|
||||
if (FeatureFlags.callingFieldTrialAnyAddressPortsKillSwitch()) {
|
||||
if (RemoteConfig.callingFieldTrialAnyAddressPortsKillSwitch()) {
|
||||
fieldTrials.put("RingRTC-AnyAddressPortsKillSwitch", "Enabled");
|
||||
}
|
||||
if (!SignalStore.internalValues().callingDisableLBRed()) {
|
||||
if (!SignalStore.internal().callingDisableLBRed()) {
|
||||
fieldTrials.put("RingRTC-Audio-LBRed-For-Opus", "Enabled,bitrate_pri:22000");
|
||||
}
|
||||
CallManager.initialize(this, new RingRtcLogger(), fieldTrials);
|
||||
@@ -448,7 +452,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
@WorkerThread
|
||||
private void initializeCircumvention() {
|
||||
if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) {
|
||||
if (AppDependencies.getSignalServiceNetworkAccess().isCensored()) {
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(ApplicationContext.this);
|
||||
} catch (Throwable t) {
|
||||
@@ -458,21 +462,21 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
|
||||
private void ensureProfileUploaded() {
|
||||
if (SignalStore.account().isRegistered() && !SignalStore.registrationValues().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
|
||||
if (SignalStore.account().isRegistered() && !SignalStore.registration().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
|
||||
Log.w(TAG, "User has a profile, but has not uploaded one. Uploading now.");
|
||||
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
|
||||
AppDependencies.getJobManager().add(new ProfileUploadJob());
|
||||
}
|
||||
}
|
||||
|
||||
private void executePendingContactSync() {
|
||||
if (TextSecurePreferences.needsFullContactSync(this)) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
|
||||
AppDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void beginJobLoop() {
|
||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||
AppDependencies.getJobManager().beginJobLoop();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
||||
@@ -28,11 +28,11 @@ import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar;
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
@@ -91,16 +91,18 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
Recipient.live(recipientId).observe(this, recipient -> {
|
||||
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient)
|
||||
: recipient.getContactPhoto();
|
||||
FallbackContactPhoto fallbackPhoto = recipient.isSelf() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
|
||||
: recipient.getFallbackContactPhoto();
|
||||
FallbackAvatar fallbackAvatar = recipient.isSelf() ? new FallbackAvatar.Resource.Person(recipient.getAvatarColor())
|
||||
: recipient.getFallbackAvatar();
|
||||
|
||||
Drawable fallbackDrawable = new FallbackAvatarDrawable(context, fallbackAvatar);
|
||||
|
||||
Resources resources = this.getResources();
|
||||
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(contactPhoto)
|
||||
.fallback(fallbackPhoto.asCallCard(this))
|
||||
.error(fallbackPhoto.asCallCard(this))
|
||||
.fallback(fallbackDrawable)
|
||||
.error(fallbackDrawable)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.addListener(new RequestListener<Bitmap>() {
|
||||
@Override
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.ConfigurationUtil;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
@@ -47,7 +47,7 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onStart() {
|
||||
logEvent("onStart()");
|
||||
ApplicationDependencies.getShakeToReport().registerActivity(this);
|
||||
AppDependencies.getShakeToReport().registerActivity(this);
|
||||
super.onStart();
|
||||
}
|
||||
|
||||
|
||||
@@ -125,12 +125,13 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
|
||||
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
|
||||
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
|
||||
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onEditedIndicatorClicked(@NonNull ConversationMessage conversationMessage);
|
||||
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
|
||||
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
|
||||
void onShowSafetyTips(boolean forGroup);
|
||||
void onReportSpamLearnMoreClicked();
|
||||
void onMessageRequestAcceptOptionsClicked();
|
||||
void onItemDoubleClick(MultiselectPart multiselectPart);
|
||||
void onPaymentTombstoneClicked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ class BiometricDeviceAuthentication(
|
||||
private val DISALLOWED_BIOMETRIC_VERSIONS = setOf(28, 29)
|
||||
}
|
||||
|
||||
fun canAuthenticate(): Boolean {
|
||||
return biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
}
|
||||
|
||||
fun authenticate(context: Context, force: Boolean, showConfirmDeviceCredentialIntent: () -> Unit): Boolean {
|
||||
val isKeyGuardSecure = ServiceUtil.getKeyguardManager(context).isKeyguardSecure
|
||||
|
||||
|
||||
@@ -29,14 +29,10 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DisplayMetricsUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
@@ -337,7 +337,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
selectionLimit,
|
||||
new ContactSearchAdapter.DisplayOptions(
|
||||
isMulti,
|
||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
|
||||
newCallCallback != null,
|
||||
false
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.qr.kitkat.ScanListener;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@@ -191,7 +191,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
|
||||
try {
|
||||
Context context = DeviceActivity.this;
|
||||
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
SignalServiceAccountManager accountManager = AppDependencies.getSignalServiceAccountManager();
|
||||
String verificationCode = accountManager.getNewDeviceVerificationCode();
|
||||
String ephemeralId = uri.getQueryParameter("uuid");
|
||||
String publicKeyEncoded = uri.getQueryParameter("pub_key");
|
||||
|
||||
@@ -25,7 +25,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -60,7 +60,7 @@ public class DeviceListFragment extends ListFragment
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
|
||||
this.accountManager = AppDependencies.getSignalServiceAccountManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,9 +17,8 @@ import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.devicetransfer.TransferStatus;
|
||||
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.InternalValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
|
||||
@@ -28,12 +27,11 @@ import org.thoughtcrime.securesms.pin.PinRestoreActivity;
|
||||
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2Activity;
|
||||
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
|
||||
import org.thoughtcrime.securesms.restore.RestoreActivity;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -55,7 +53,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
private static final int STATE_TRANSFER_ONGOING = 8;
|
||||
private static final int STATE_TRANSFER_LOCKED = 9;
|
||||
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
|
||||
private static final int STATE_RESTORE_BACKUP = 11;
|
||||
private static final int STATE_TRANSFER_OR_RESTORE = 11;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -64,7 +62,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
protected final void onCreate(Bundle savedInstanceState) {
|
||||
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
|
||||
AppStartup.getInstance().onCriticalRenderEventStart();
|
||||
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
|
||||
this.networkAccess = AppDependencies.getSignalServiceNetworkAccess();
|
||||
onPreCreate();
|
||||
|
||||
final boolean locked = KeyCachingService.isLocked(this);
|
||||
@@ -93,7 +91,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
@Override
|
||||
public void onMasterSecretCleared() {
|
||||
Log.d(TAG, "onMasterSecretCleared()");
|
||||
if (ApplicationDependencies.getAppForegroundObserver().isForegrounded()) routeApplicationState(true);
|
||||
if (AppDependencies.getAppForegroundObserver().isForegrounded()) routeApplicationState(true);
|
||||
else finish();
|
||||
}
|
||||
|
||||
@@ -153,7 +151,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
|
||||
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
|
||||
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
|
||||
case STATE_RESTORE_BACKUP: return getRestoreIntent();
|
||||
case STATE_TRANSFER_OR_RESTORE: return getTransferOrRestoreIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -167,12 +165,12 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return STATE_UI_BLOCKING_UPGRADE;
|
||||
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
|
||||
return STATE_WELCOME_PUSH_SCREEN;
|
||||
} else if (SignalStore.internalValues().enterRestoreV2Flow()) {
|
||||
return STATE_RESTORE_BACKUP;
|
||||
} else if (SignalStore.storageService().needsAccountRestore()) {
|
||||
return STATE_ENTER_SIGNAL_PIN;
|
||||
} else if (userHasSkippedOrForgottenPin()) {
|
||||
return STATE_CREATE_SIGNAL_PIN;
|
||||
} else if (userCanTransferOrRestore()) {
|
||||
return STATE_TRANSFER_OR_RESTORE;
|
||||
} else if (userMustSetProfileName()) {
|
||||
return STATE_CREATE_PROFILE_NAME;
|
||||
} else if (userMustCreateSignalPin()) {
|
||||
@@ -188,16 +186,20 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
}
|
||||
|
||||
private boolean userCanTransferOrRestore() {
|
||||
return !SignalStore.registration().isRegistrationComplete() && RemoteConfig.restoreAfterRegistration() && !SignalStore.registration().hasSkippedTransferOrRestore();
|
||||
}
|
||||
|
||||
private boolean userMustCreateSignalPin() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().lastPinCreateFailed() && !SignalStore.svr().hasOptedOut();
|
||||
return !SignalStore.registration().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().lastPinCreateFailed() && !SignalStore.svr().hasOptedOut();
|
||||
}
|
||||
|
||||
private boolean userHasSkippedOrForgottenPin() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().hasOptedOut() && SignalStore.svr().isPinForgottenOrSkipped();
|
||||
return !SignalStore.registration().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().hasOptedOut() && SignalStore.svr().isPinForgottenOrSkipped();
|
||||
}
|
||||
|
||||
private boolean userMustSetProfileName() {
|
||||
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
|
||||
return !SignalStore.registration().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
|
||||
}
|
||||
|
||||
private Intent getCreatePassphraseIntent() {
|
||||
@@ -206,7 +208,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
|
||||
private Intent getPromptPassphraseIntent() {
|
||||
Intent intent = getRoutedIntent(PassphrasePromptActivity.class, getIntent());
|
||||
intent.putExtra(PassphrasePromptActivity.FROM_FOREGROUND, ApplicationDependencies.getAppForegroundObserver().isForegrounded());
|
||||
intent.putExtra(PassphrasePromptActivity.FROM_FOREGROUND, AppDependencies.getAppForegroundObserver().isForegrounded());
|
||||
return intent;
|
||||
}
|
||||
|
||||
@@ -218,11 +220,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private Intent getPushRegistrationIntent() {
|
||||
if (FeatureFlags.registrationV2()) {
|
||||
return RegistrationV2Activity.newIntentForNewRegistration(this, getIntent());
|
||||
} else {
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
}
|
||||
return RegistrationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
}
|
||||
|
||||
private Intent getEnterSignalPinIntent() {
|
||||
@@ -241,8 +239,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return getRoutedIntent(CreateSvrPinActivity.class, intent);
|
||||
}
|
||||
|
||||
private Intent getRestoreIntent() {
|
||||
Intent intent = RestoreActivity.getIntentForRestore(this);
|
||||
private Intent getTransferOrRestoreIntent() {
|
||||
Intent intent = RestoreActivity.getIntentForTransferOrRestore(this);
|
||||
return getRoutedIntent(intent, getIntent());
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,14 @@ import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Rational;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.core.app.PictureInPictureModeChangedInfo;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LiveDataReactiveStreams;
|
||||
@@ -80,7 +81,7 @@ import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoView
|
||||
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
|
||||
import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.events.WebRtcViewModel;
|
||||
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@@ -93,7 +94,7 @@ import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
|
||||
@@ -116,6 +117,7 @@ import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
import static org.thoughtcrime.securesms.permissions.PermissionDeniedBottomSheet.showPermissionFragment;
|
||||
|
||||
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback, ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
|
||||
|
||||
@@ -165,7 +167,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private boolean enterPipOnResume;
|
||||
private long lastProcessedIntentTimestamp;
|
||||
private WebRtcViewModel previousEvent = null;
|
||||
|
||||
private boolean isAskingForPermission;
|
||||
private Disposable ephemeralStateDisposable = Disposable.empty();
|
||||
|
||||
@Override
|
||||
@@ -237,6 +239,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
initializePendingParticipantFragmentListener();
|
||||
|
||||
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface));
|
||||
|
||||
if (!hasCameraPermission() & !hasAudioPermission()) {
|
||||
askCameraAudioPermissions(() -> handleSetMuteVideo(false));
|
||||
} else if (!hasAudioPermission()) {
|
||||
askAudioPermissions(() -> {});
|
||||
}
|
||||
}
|
||||
|
||||
private void registerSystemPipChangeListeners() {
|
||||
@@ -250,10 +258,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
ephemeralStateDisposable = ApplicationDependencies.getSignalCallManager()
|
||||
.ephemeralStates()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(viewModel::updateFromEphemeralState);
|
||||
ephemeralStateDisposable = AppDependencies.getSignalCallManager()
|
||||
.ephemeralStates()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(viewModel::updateFromEphemeralState);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -299,7 +307,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
Log.i(TAG, "onPause");
|
||||
super.onPause();
|
||||
|
||||
if (!viewModel.isCallStarting()) {
|
||||
if (!isAskingForPermission && !viewModel.isCallStarting()) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
finish();
|
||||
@@ -319,15 +327,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
requestNewSizesThrottle.clear();
|
||||
}
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().setEnableVideo(false);
|
||||
AppDependencies.getSignalCallManager().setEnableVideo(false);
|
||||
|
||||
if (!viewModel.isCallStarting()) {
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null) {
|
||||
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
AppDependencies.getSignalCallManager().cancelPreJoin();
|
||||
} else if (state.getCallState().getInOngoingCall() && isInPipMode()) {
|
||||
ApplicationDependencies.getSignalCallManager().relaunchPipOnForeground();
|
||||
AppDependencies.getSignalCallManager().relaunchPipOnForeground();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,7 +415,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void initializePendingParticipantFragmentListener() {
|
||||
if (!FeatureFlags.adHocCalling()) {
|
||||
if (!RemoteConfig.adHocCalling()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -432,7 +440,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.WebRtcCallActivity__approve_all, (dialog, which) -> {
|
||||
for (RecipientId id : recipientIds) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id);
|
||||
AppDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
@@ -444,7 +452,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.WebRtcCallActivity__deny_all, (dialog, which) -> {
|
||||
for (RecipientId id : recipientIds) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id);
|
||||
AppDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
@@ -512,12 +520,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null) {
|
||||
if (state.needsNewRequestSizes()) {
|
||||
requestNewSizesThrottle.publish(() -> ApplicationDependencies.getSignalCallManager().updateRenderedResolutions());
|
||||
requestNewSizesThrottle.publish(() -> AppDependencies.getSignalCallManager().updateRenderedResolutions());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
|
||||
viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> AppDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
|
||||
viewModel.getControlsRotation().observe(this, callScreen::rotateControls);
|
||||
|
||||
addOnPictureInPictureModeChangedListener(info -> {
|
||||
@@ -643,83 +651,66 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void handleSetAudioHandset() {
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.EARPIECE));
|
||||
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.EARPIECE));
|
||||
}
|
||||
|
||||
private void handleSetAudioSpeaker() {
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.SPEAKER_PHONE));
|
||||
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.SPEAKER_PHONE));
|
||||
}
|
||||
|
||||
private void handleSetAudioBluetooth() {
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.BLUETOOTH));
|
||||
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.BLUETOOTH));
|
||||
}
|
||||
|
||||
private void handleSetAudioWiredHeadset() {
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.WIRED_HEADSET));
|
||||
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.WIRED_HEADSET));
|
||||
}
|
||||
|
||||
private void handleSetMuteAudio(boolean enabled) {
|
||||
ApplicationDependencies.getSignalCallManager().setMuteAudio(enabled);
|
||||
AppDependencies.getSignalCallManager().setMuteAudio(enabled);
|
||||
}
|
||||
|
||||
private void handleSetMuteVideo(boolean muted) {
|
||||
Recipient recipient = viewModel.getRecipient().get();
|
||||
|
||||
if (!recipient.equals(Recipient.UNKNOWN)) {
|
||||
String recipientDisplayName = recipient.getDisplayName(this);
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName), R.drawable.ic_video_solid_24_tinted)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName))
|
||||
.onAllGranted(() -> ApplicationDependencies.getSignalCallManager().setEnableVideo(!muted))
|
||||
.execute();
|
||||
Runnable onGranted = () -> AppDependencies.getSignalCallManager().setEnableVideo(!muted);
|
||||
askCameraPermissions(onGranted);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleFlipCamera() {
|
||||
ApplicationDependencies.getSignalCallManager().flipCamera();
|
||||
AppDependencies.getSignalCallManager().flipCamera();
|
||||
}
|
||||
|
||||
private void handleAnswerWithAudio() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone),
|
||||
R.drawable.ic_mic_solid_24)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
.onAllGranted(() -> {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().acceptCall(false);
|
||||
})
|
||||
.onAnyDenied(this::handleDenyCall)
|
||||
.execute();
|
||||
Runnable onGranted = () -> {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
AppDependencies.getSignalCallManager().acceptCall(false);
|
||||
};
|
||||
askAudioPermissions(onGranted);
|
||||
}
|
||||
|
||||
private void handleAnswerWithVideo() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone_and_camera), R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
|
||||
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
|
||||
.onAllGranted(() -> {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
|
||||
ApplicationDependencies.getSignalCallManager().acceptCall(true);
|
||||
|
||||
handleSetMuteVideo(false);
|
||||
})
|
||||
.onAnyDenied(this::handleDenyCall)
|
||||
.execute();
|
||||
Runnable onGranted = () -> {
|
||||
callScreen.setStatus(getString(R.string.RedPhone_answering));
|
||||
AppDependencies.getSignalCallManager().acceptCall(true);
|
||||
handleSetMuteVideo(false);
|
||||
};
|
||||
if (!hasCameraPermission() &!hasAudioPermission()) {
|
||||
askCameraAudioPermissions(onGranted);
|
||||
} else if (!hasAudioPermission()) {
|
||||
askAudioPermissions(onGranted);
|
||||
} else {
|
||||
askCameraPermissions(onGranted);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDenyCall() {
|
||||
Recipient recipient = viewModel.getRecipient().get();
|
||||
|
||||
if (!recipient.equals(Recipient.UNKNOWN)) {
|
||||
ApplicationDependencies.getSignalCallManager().denyCall();
|
||||
AppDependencies.getSignalCallManager().denyCall();
|
||||
|
||||
callScreen.setRecipient(recipient);
|
||||
callScreen.setStatus(getString(R.string.RedPhone_ending_call));
|
||||
@@ -729,7 +720,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
private void handleEndCall() {
|
||||
Log.i(TAG, "Hangup pressed, handling termination now...");
|
||||
ApplicationDependencies.getSignalCallManager().localHangup();
|
||||
AppDependencies.getSignalCallManager().localHangup();
|
||||
}
|
||||
|
||||
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
|
||||
@@ -827,13 +818,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void updateGroupMembersForGroupCall() {
|
||||
ApplicationDependencies.getSignalCallManager().requestUpdateGroupMembers();
|
||||
AppDependencies.getSignalCallManager().requestUpdateGroupMembers();
|
||||
}
|
||||
|
||||
public void handleGroupMemberCountChange(int count) {
|
||||
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize();
|
||||
boolean canRing = count <= RemoteConfig.maxGroupCallRingSize();
|
||||
callScreen.enableRingGroup(canRing);
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
|
||||
AppDependencies.getSignalCallManager().setRingGroup(canRing);
|
||||
}
|
||||
|
||||
private void updateSpeakerHint(boolean showSpeakerHint) {
|
||||
@@ -857,7 +848,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
if (state.getGroupCallState().isConnected()) {
|
||||
ApplicationDependencies.getSignalCallManager().groupApproveSafetyChange(changedRecipients);
|
||||
AppDependencies.getSignalCallManager().groupApproveSafetyChange(changedRecipients);
|
||||
} else {
|
||||
viewModel.startCall(state.getLocalParticipant().isVideoEnabled());
|
||||
}
|
||||
@@ -871,7 +862,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
|
||||
if (state != null && state.getGroupCallState().isNotIdle()) {
|
||||
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
|
||||
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
|
||||
AppDependencies.getSignalCallManager().cancelPreJoin();
|
||||
finish();
|
||||
} else {
|
||||
handleEndCall();
|
||||
@@ -991,18 +982,97 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
callScreen.setRingGroup(event.shouldRingGroup());
|
||||
|
||||
if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) {
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
|
||||
AppDependencies.getSignalCallManager().setRingGroup(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasCameraPermission() {
|
||||
return Permissions.hasAll(this, Manifest.permission.CAMERA);
|
||||
}
|
||||
|
||||
private boolean hasAudioPermission() {
|
||||
return Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO);
|
||||
}
|
||||
|
||||
private void askCameraPermissions(@NonNull Runnable onGranted) {
|
||||
if (!isAskingForPermission) {
|
||||
isAskingForPermission = true;
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity__allow_access_camera), getString(R.string.WebRtcCallActivity__to_enable_video_allow_camera), false, R.drawable.symbol_video_24)
|
||||
.onAnyResult(() -> isAskingForPermission = false)
|
||||
.onAllGranted(() -> {
|
||||
onGranted.run();
|
||||
findViewById(R.id.missing_permissions_container).setVisibility(View.GONE);
|
||||
})
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_camera_access_enable_video, Toast.LENGTH_LONG).show())
|
||||
.onAnyPermanentlyDenied(() -> showPermissionFragment(R.string.WebRtcCallActivity__allow_access_camera, R.string.WebRtcCallActivity__to_enable_video).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void askAudioPermissions(@NonNull Runnable onGranted) {
|
||||
if (!isAskingForPermission) {
|
||||
isAskingForPermission = true;
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity__allow_access_microphone), getString(R.string.WebRtcCallActivity__to_start_call_microphone), false, R.drawable.ic_mic_24)
|
||||
.onAnyResult(() -> isAskingForPermission = false)
|
||||
.onAllGranted(onGranted)
|
||||
.onAnyDenied(() -> {
|
||||
Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_microphone_start_call, Toast.LENGTH_LONG).show();
|
||||
handleDenyCall();
|
||||
})
|
||||
.onAnyPermanentlyDenied(() -> showPermissionFragment(R.string.WebRtcCallActivity__allow_access_microphone, R.string.WebRtcCallActivity__to_start_call).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG))
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
public void askCameraAudioPermissions(@NonNull Runnable onGranted) {
|
||||
if (!isAskingForPermission) {
|
||||
isAskingForPermission = true;
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withRationaleDialog(getString(R.string.WebRtcCallActivity__allow_access_camera_microphone), getString(R.string.WebRtcCallActivity__to_start_call_camera_microphone), false, R.drawable.ic_mic_24, R.drawable.symbol_video_24)
|
||||
.onAnyResult(() -> isAskingForPermission = false)
|
||||
.onSomePermanentlyDenied(deniedPermissions -> {
|
||||
if (deniedPermissions.containsAll(List.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))) {
|
||||
showPermissionFragment(R.string.WebRtcCallActivity__allow_access_camera_microphone, R.string.WebRtcCallActivity__to_start_call).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
} else if (deniedPermissions.contains(Manifest.permission.CAMERA)) {
|
||||
showPermissionFragment(R.string.WebRtcCallActivity__allow_access_camera, R.string.WebRtcCallActivity__to_enable_video).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
} else {
|
||||
showPermissionFragment(R.string.WebRtcCallActivity__allow_access_microphone, R.string.WebRtcCallActivity__to_start_call).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
|
||||
}
|
||||
})
|
||||
.onAllGranted(onGranted)
|
||||
.onSomeGranted(permissions -> {
|
||||
if (permissions.contains(Manifest.permission.CAMERA)) {
|
||||
findViewById(R.id.missing_permissions_container).setVisibility(View.GONE);
|
||||
}
|
||||
})
|
||||
.onSomeDenied(deniedPermissions -> {
|
||||
if (deniedPermissions.contains(Manifest.permission.RECORD_AUDIO)) {
|
||||
Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_microphone_start_call, Toast.LENGTH_LONG).show();
|
||||
handleDenyCall();
|
||||
} else {
|
||||
Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_camera_access_enable_video, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
private void startCall(boolean isVideoCall) {
|
||||
enableVideoIfAvailable = isVideoCall;
|
||||
|
||||
if (isVideoCall) {
|
||||
ApplicationDependencies.getSignalCallManager().startOutgoingVideoCall(viewModel.getRecipient().get());
|
||||
AppDependencies.getSignalCallManager().startOutgoingVideoCall(viewModel.getRecipient().get());
|
||||
} else {
|
||||
ApplicationDependencies.getSignalCallManager().startOutgoingAudioCall(viewModel.getRecipient().get());
|
||||
AppDependencies.getSignalCallManager().startOutgoingAudioCall(viewModel.getRecipient().get());
|
||||
}
|
||||
|
||||
MessageSender.onMessageSent();
|
||||
@@ -1013,7 +1083,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@Override
|
||||
public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
|
||||
ApplicationDependencies.getSignalCallManager().react(emoji);
|
||||
AppDependencies.getSignalCallManager().react(emoji);
|
||||
callOverflowPopupWindow.dismiss();
|
||||
}
|
||||
|
||||
@@ -1032,11 +1102,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
@Override
|
||||
public void toggleControls() {
|
||||
WebRtcControls controlState = viewModel.getWebRtcControls().getValue();
|
||||
if (controlState != null && !controlState.displayIncomingCallButtons()) {
|
||||
if (controlState != null && !controlState.displayIncomingCallButtons() && !controlState.displayErrorControls()) {
|
||||
controlsAndInfo.toggleControls();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioPermissionsRequested(Runnable onGranted) {
|
||||
askAudioPermissions(onGranted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
|
||||
maybeDisplaySpeakerphonePopup(audioOutput);
|
||||
@@ -1062,7 +1137,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
@Override
|
||||
public void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput) {
|
||||
maybeDisplaySpeakerphonePopup(audioOutput.getWebRtcAudioOutput());
|
||||
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId()));
|
||||
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1072,9 +1147,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@Override
|
||||
public void onMicChanged(boolean isMicEnabled) {
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallStateUpdatePopupWindow.CallStateUpdate.MIC_ON
|
||||
: CallStateUpdatePopupWindow.CallStateUpdate.MIC_OFF);
|
||||
handleSetMuteAudio(!isMicEnabled);
|
||||
Runnable onGranted = () -> {
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallStateUpdatePopupWindow.CallStateUpdate.MIC_ON
|
||||
: CallStateUpdatePopupWindow.CallStateUpdate.MIC_OFF);
|
||||
handleSetMuteAudio(!isMicEnabled);
|
||||
};
|
||||
askAudioPermissions(onGranted);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1125,11 +1203,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
@Override
|
||||
public void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed) {
|
||||
if (ringingAllowed) {
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(ringGroup);
|
||||
AppDependencies.getSignalCallManager().setRingGroup(ringGroup);
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(ringGroup ? CallStateUpdatePopupWindow.CallStateUpdate.RINGING_ON
|
||||
: CallStateUpdatePopupWindow.CallStateUpdate.RINGING_OFF);
|
||||
} else {
|
||||
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
|
||||
AppDependencies.getSignalCallManager().setRingGroup(false);
|
||||
callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.RINGING_DISABLED);
|
||||
}
|
||||
}
|
||||
@@ -1158,12 +1236,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@Override
|
||||
public void onAllowPendingRecipient(@NonNull Recipient pendingRecipient) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId());
|
||||
AppDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRejectPendingRecipient(@NonNull Recipient pendingRecipient) {
|
||||
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId());
|
||||
AppDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1209,9 +1287,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@Override
|
||||
public void onHidden() {
|
||||
fullscreenHelper.hideSystemUI();
|
||||
if (videoTooltip != null) {
|
||||
videoTooltip.dismiss();
|
||||
WebRtcControls controlState = viewModel.getWebRtcControls().getValue();
|
||||
if (controlState == null || !controlState.displayErrorControls()) {
|
||||
fullscreenHelper.hideSystemUI();
|
||||
if (videoTooltip != null) {
|
||||
videoTooltip.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class SignalBackupAgent : BackupAgent() {
|
||||
)
|
||||
|
||||
override fun onBackup(oldState: ParcelFileDescriptor?, data: BackupDataOutput, newState: ParcelFileDescriptor) {
|
||||
Log.i(TAG, "Performing backup to Android Backup Service.")
|
||||
val contentsHash = cumulativeHashCode()
|
||||
if (oldState == null) {
|
||||
performBackup(data)
|
||||
@@ -36,9 +37,11 @@ class SignalBackupAgent : BackupAgent() {
|
||||
}
|
||||
|
||||
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(contentsHash) }
|
||||
Log.i(TAG, "Backup finished.")
|
||||
}
|
||||
|
||||
private fun performBackup(data: BackupDataOutput) {
|
||||
Log.i(TAG, "Creating new backup data.")
|
||||
items.forEach {
|
||||
val backupData = it.getDataForBackup()
|
||||
data.writeEntityHeader(it.getKey(), backupData.size)
|
||||
@@ -54,7 +57,7 @@ class SignalBackupAgent : BackupAgent() {
|
||||
items.find { dataInput.key == it.getKey() }?.restoreData(buffer)
|
||||
}
|
||||
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) }
|
||||
Log.i(TAG, "Android Backup Service complete.")
|
||||
Log.i(TAG, "Android Backup Service restore complete.")
|
||||
}
|
||||
|
||||
private fun cumulativeHashCode(): Int {
|
||||
@@ -62,6 +65,6 @@ class SignalBackupAgent : BackupAgent() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SignalBackupAgent"
|
||||
private val TAG = Log.tag(SignalBackupAgent::class)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,19 +17,19 @@ object SvrAuthTokens : AndroidBackupItem {
|
||||
}
|
||||
|
||||
override fun getDataForBackup(): ByteArray {
|
||||
val proto = SvrAuthToken(tokens = SignalStore.svr().authTokenList)
|
||||
val proto = SvrAuthToken(svr2Tokens = SignalStore.svr.svr2AuthTokens)
|
||||
return proto.encode()
|
||||
}
|
||||
|
||||
override fun restoreData(data: ByteArray) {
|
||||
if (SignalStore.svr().authTokenList.isNotEmpty()) {
|
||||
if (SignalStore.svr.svr2AuthTokens.isNotEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val proto = SvrAuthToken.ADAPTER.decode(data)
|
||||
|
||||
SignalStore.svr().putAuthTokenList(proto.tokens)
|
||||
SignalStore.svr.putSvr2AuthTokens(proto.svr2Tokens)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Cannot restore KbsAuthToken from backup service.")
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class ApkUpdateDownloadManagerReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2)
|
||||
if (downloadId != SignalStore.apkUpdate().downloadId) {
|
||||
if (downloadId != SignalStore.apkUpdate.downloadId) {
|
||||
Log.w(TAG, "downloadId doesn't match the one we're waiting for! Ignoring.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.signal.core.util.PendingIntentFlags
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.getDownloadManager
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.ApkUpdateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.Environment
|
||||
@@ -38,30 +38,30 @@ object ApkUpdateInstaller {
|
||||
* [userInitiated] = true, and then everything installs.
|
||||
*/
|
||||
fun installOrPromptForInstall(context: Context, downloadId: Long, userInitiated: Boolean) {
|
||||
if (downloadId != SignalStore.apkUpdate().downloadId) {
|
||||
Log.w(TAG, "DownloadId doesn't match the one we're waiting for (current: $downloadId, expected: ${SignalStore.apkUpdate().downloadId})! We likely have newer data. Ignoring.")
|
||||
if (downloadId != SignalStore.apkUpdate.downloadId) {
|
||||
Log.w(TAG, "DownloadId doesn't match the one we're waiting for (current: $downloadId, expected: ${SignalStore.apkUpdate.downloadId})! We likely have newer data. Ignoring.")
|
||||
ApkUpdateNotifications.dismissInstallPrompt(context)
|
||||
ApplicationDependencies.getJobManager().add(ApkUpdateJob())
|
||||
AppDependencies.jobManager.add(ApkUpdateJob())
|
||||
return
|
||||
}
|
||||
|
||||
val digest = SignalStore.apkUpdate().digest
|
||||
val digest = SignalStore.apkUpdate.digest
|
||||
if (digest == null) {
|
||||
Log.w(TAG, "DownloadId matches, but digest is null! Inconsistent state. Failing and clearing state.")
|
||||
SignalStore.apkUpdate().clearDownloadAttributes()
|
||||
SignalStore.apkUpdate.clearDownloadAttributes()
|
||||
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isMatchingDigest(context, downloadId, digest)) {
|
||||
Log.w(TAG, "DownloadId matches, but digest does not! Bad download or inconsistent state. Failing and clearing state.")
|
||||
SignalStore.apkUpdate().clearDownloadAttributes()
|
||||
SignalStore.apkUpdate.clearDownloadAttributes()
|
||||
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
|
||||
return
|
||||
}
|
||||
|
||||
if (!userInitiated && !shouldAutoUpdate()) {
|
||||
Log.w(TAG, "Not user-initiated and not eligible for auto-update. Prompting. (API=${Build.VERSION.SDK_INT}, Foreground=${ApplicationDependencies.getAppForegroundObserver().isForegrounded}, AutoUpdate=${SignalStore.apkUpdate().autoUpdate})")
|
||||
Log.w(TAG, "Not user-initiated and not eligible for auto-update. Prompting. (API=${Build.VERSION.SDK_INT}, Foreground=${AppDependencies.appForegroundObserver.isForegrounded}, AutoUpdate=${SignalStore.apkUpdate.autoUpdate})")
|
||||
ApkUpdateNotifications.showInstallPrompt(context, downloadId)
|
||||
return
|
||||
}
|
||||
@@ -70,11 +70,11 @@ object ApkUpdateInstaller {
|
||||
installApk(context, downloadId, userInitiated)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Hit IOException when trying to install APK!", e)
|
||||
SignalStore.apkUpdate().clearDownloadAttributes()
|
||||
SignalStore.apkUpdate.clearDownloadAttributes()
|
||||
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
|
||||
} catch (e: SecurityException) {
|
||||
Log.w(TAG, "Hit SecurityException when trying to install APK!", e)
|
||||
SignalStore.apkUpdate().clearDownloadAttributes()
|
||||
SignalStore.apkUpdate.clearDownloadAttributes()
|
||||
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,6 @@ object ApkUpdateInstaller {
|
||||
|
||||
private fun shouldAutoUpdate(): Boolean {
|
||||
// TODO Auto-updates temporarily restricted to nightlies. Once we have designs for allowing users to opt-out of auto-updates, we can re-enable this
|
||||
return Environment.IS_NIGHTLY && Build.VERSION.SDK_INT >= 31 && SignalStore.apkUpdate().autoUpdate && !ApplicationDependencies.getAppForegroundObserver().isForegrounded
|
||||
return Environment.IS_NIGHTLY && Build.VERSION.SDK_INT >= 31 && SignalStore.apkUpdate.autoUpdate && !AppDependencies.appForegroundObserver.isForegrounded
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ class ApkUpdatePackageInstallerReceiver : BroadcastReceiver() {
|
||||
|
||||
when (statusCode) {
|
||||
PackageInstaller.STATUS_SUCCESS -> {
|
||||
if (SignalStore.apkUpdate().lastApkUploadTime != SignalStore.apkUpdate().pendingApkUploadTime) {
|
||||
Log.i(TAG, "Update installed successfully! Updating our lastApkUploadTime to ${SignalStore.apkUpdate().pendingApkUploadTime}")
|
||||
SignalStore.apkUpdate().lastApkUploadTime = SignalStore.apkUpdate().pendingApkUploadTime
|
||||
if (SignalStore.apkUpdate.lastApkUploadTime != SignalStore.apkUpdate.pendingApkUploadTime) {
|
||||
Log.i(TAG, "Update installed successfully! Updating our lastApkUploadTime to ${SignalStore.apkUpdate.pendingApkUploadTime}")
|
||||
SignalStore.apkUpdate.lastApkUploadTime = SignalStore.apkUpdate.pendingApkUploadTime
|
||||
ApkUpdateNotifications.showAutoUpdateSuccess(context)
|
||||
} else {
|
||||
Log.i(TAG, "Spurious 'success' notification?")
|
||||
|
||||
@@ -10,7 +10,7 @@ import android.content.Context;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.ApkUpdateJob;
|
||||
import org.thoughtcrime.securesms.service.PersistentAlarmManagerListener;
|
||||
import org.thoughtcrime.securesms.util.Environment;
|
||||
@@ -35,7 +35,7 @@ public class ApkUpdateRefreshListener extends PersistentAlarmManagerListener {
|
||||
|
||||
if (scheduledTime != 0 && BuildConfig.MANAGES_APP_UPDATES) {
|
||||
Log.i(TAG, "Queueing APK update job...");
|
||||
ApplicationDependencies.getJobManager().add(new ApkUpdateJob());
|
||||
AppDependencies.getJobManager().add(new ApkUpdateJob());
|
||||
}
|
||||
|
||||
long newTime = System.currentTimeMillis() + INTERVAL;
|
||||
|
||||
@@ -10,6 +10,8 @@ import android.os.Parcel
|
||||
import org.signal.core.util.Base64
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import java.util.UUID
|
||||
|
||||
class ArchivedAttachment : Attachment {
|
||||
|
||||
@@ -44,8 +46,10 @@ class ArchivedAttachment : Attachment {
|
||||
blurHash: String?,
|
||||
voiceNote: Boolean,
|
||||
borderless: Boolean,
|
||||
stickerLocator: StickerLocator?,
|
||||
gif: Boolean,
|
||||
quote: Boolean
|
||||
quote: Boolean,
|
||||
uuid: UUID?
|
||||
) : super(
|
||||
contentType = contentType ?: "",
|
||||
quote = quote,
|
||||
@@ -66,10 +70,11 @@ class ArchivedAttachment : Attachment {
|
||||
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
|
||||
uploadTimestamp = 0,
|
||||
caption = caption,
|
||||
stickerLocator = null,
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = BlurHash.parseOrNull(blurHash),
|
||||
audioHash = null,
|
||||
transformProperties = null
|
||||
transformProperties = null,
|
||||
uuid = uuid
|
||||
) {
|
||||
this.archiveCdn = archiveCdn ?: Cdn.CDN_3.cdnNumber
|
||||
this.archiveMediaName = archiveMediaName
|
||||
|
||||
@@ -14,6 +14,8 @@ import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Note: We have to use our own Parcelable implementation because we need to do custom stuff to preserve
|
||||
@@ -65,12 +67,16 @@ abstract class Attachment(
|
||||
@JvmField
|
||||
val audioHash: AudioHash?,
|
||||
@JvmField
|
||||
val transformProperties: TransformProperties?
|
||||
val transformProperties: TransformProperties?,
|
||||
@JvmField
|
||||
val uuid: UUID?
|
||||
) : Parcelable {
|
||||
|
||||
abstract val uri: Uri?
|
||||
abstract val publicUri: Uri?
|
||||
abstract val thumbnailUri: Uri?
|
||||
val displayUri: Uri?
|
||||
get() = uri ?: thumbnailUri
|
||||
|
||||
protected constructor(parcel: Parcel) : this(
|
||||
contentType = parcel.readString()!!,
|
||||
@@ -95,7 +101,8 @@ abstract class Attachment(
|
||||
stickerLocator = ParcelCompat.readParcelable(parcel, StickerLocator::class.java.classLoader, StickerLocator::class.java),
|
||||
blurHash = ParcelCompat.readParcelable(parcel, BlurHash::class.java.classLoader, BlurHash::class.java),
|
||||
audioHash = ParcelCompat.readParcelable(parcel, AudioHash::class.java.classLoader, AudioHash::class.java),
|
||||
transformProperties = ParcelCompat.readParcelable(parcel, TransformProperties::class.java.classLoader, TransformProperties::class.java)
|
||||
transformProperties = ParcelCompat.readParcelable(parcel, TransformProperties::class.java.classLoader, TransformProperties::class.java),
|
||||
uuid = UuidUtil.parseOrNull(parcel.readString())
|
||||
)
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
@@ -123,6 +130,7 @@ abstract class Attachment(
|
||||
dest.writeParcelable(blurHash, 0)
|
||||
dest.writeParcelable(audioHash, 0)
|
||||
dest.writeParcelable(transformProperties, 0)
|
||||
dest.writeString(uuid?.toString())
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
|
||||
@@ -55,6 +55,7 @@ object AttachmentUploadUtil {
|
||||
.withResumableUploadSpec(ResumableUploadSpec.from(uploadSpec))
|
||||
.withCancelationSignal(cancellationSignal)
|
||||
.withListener(progressListener)
|
||||
.withUuid(attachment.uuid)
|
||||
|
||||
if (MediaUtil.isImageType(attachment.contentType)) {
|
||||
builder.withBlurHash(getImageBlurHash(context, attachment))
|
||||
|
||||
@@ -5,11 +5,12 @@ import android.os.Parcel
|
||||
import androidx.core.os.ParcelCompat
|
||||
import org.thoughtcrime.securesms.audio.AudioHash
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil
|
||||
import java.util.UUID
|
||||
|
||||
class DatabaseAttachment : Attachment {
|
||||
|
||||
@@ -37,6 +38,9 @@ class DatabaseAttachment : Attachment {
|
||||
@JvmField
|
||||
val archiveMediaId: String?
|
||||
|
||||
@JvmField
|
||||
val thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState
|
||||
|
||||
private val hasArchiveThumbnail: Boolean
|
||||
private val hasThumbnail: Boolean
|
||||
val displayOrder: Int
|
||||
@@ -75,7 +79,9 @@ class DatabaseAttachment : Attachment {
|
||||
archiveCdn: Int,
|
||||
archiveThumbnailCdn: Int,
|
||||
archiveMediaName: String?,
|
||||
archiveMediaId: String?
|
||||
archiveMediaId: String?,
|
||||
thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState,
|
||||
uuid: UUID?
|
||||
) : super(
|
||||
contentType = contentType!!,
|
||||
transferState = transferProgress,
|
||||
@@ -98,7 +104,8 @@ class DatabaseAttachment : Attachment {
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
audioHash = audioHash,
|
||||
transformProperties = transformProperties
|
||||
transformProperties = transformProperties,
|
||||
uuid = uuid
|
||||
) {
|
||||
this.attachmentId = attachmentId
|
||||
this.mmsId = mmsId
|
||||
@@ -111,6 +118,7 @@ class DatabaseAttachment : Attachment {
|
||||
this.archiveThumbnailCdn = archiveThumbnailCdn
|
||||
this.archiveMediaName = archiveMediaName
|
||||
this.archiveMediaId = archiveMediaId
|
||||
this.thumbnailRestoreState = thumbnailRestoreState
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
@@ -125,6 +133,7 @@ class DatabaseAttachment : Attachment {
|
||||
archiveMediaName = parcel.readString()
|
||||
archiveMediaId = parcel.readString()
|
||||
hasArchiveThumbnail = ParcelUtil.readBoolean(parcel)
|
||||
thumbnailRestoreState = AttachmentTable.ThumbnailRestoreState.deserialize(parcel.readInt())
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
@@ -140,10 +149,11 @@ class DatabaseAttachment : Attachment {
|
||||
dest.writeString(archiveMediaName)
|
||||
dest.writeString(archiveMediaId)
|
||||
ParcelUtil.writeBoolean(dest, hasArchiveThumbnail)
|
||||
dest.writeInt(thumbnailRestoreState.value)
|
||||
}
|
||||
|
||||
override val uri: Uri?
|
||||
get() = if (hasData || FeatureFlags.instantVideoPlayback() && getIncrementalDigest() != null) {
|
||||
get() = if (hasData || getIncrementalDigest() != null) {
|
||||
PartAuthority.getAttachmentDataUri(attachmentId)
|
||||
} else {
|
||||
null
|
||||
@@ -165,7 +175,7 @@ class DatabaseAttachment : Attachment {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other != null &&
|
||||
other is DatabaseAttachment && other.attachmentId == attachmentId
|
||||
other is DatabaseAttachment && other.attachmentId == attachmentId && other.uri == uri
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
class PointerAttachment : Attachment {
|
||||
@VisibleForTesting
|
||||
@@ -35,7 +36,8 @@ class PointerAttachment : Attachment {
|
||||
uploadTimestamp: Long,
|
||||
caption: String?,
|
||||
stickerLocator: StickerLocator?,
|
||||
blurHash: BlurHash?
|
||||
blurHash: BlurHash?,
|
||||
uuid: UUID?
|
||||
) : super(
|
||||
contentType = contentType,
|
||||
transferState = transferState,
|
||||
@@ -59,7 +61,8 @@ class PointerAttachment : Attachment {
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
audioHash = null,
|
||||
transformProperties = null
|
||||
transformProperties = null,
|
||||
uuid = uuid
|
||||
)
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel)
|
||||
@@ -115,7 +118,8 @@ class PointerAttachment : Attachment {
|
||||
uploadTimestamp = pointer.get().asPointer().uploadTimestamp,
|
||||
caption = pointer.get().asPointer().caption.orElse(null),
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = BlurHash.parseOrNull(pointer.get().asPointer().blurHash.orElse(null))
|
||||
blurHash = BlurHash.parseOrNull(pointer.get().asPointer().blurHash.orElse(null)),
|
||||
uuid = pointer.get().asPointer().uuid
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -152,7 +156,8 @@ class PointerAttachment : Attachment {
|
||||
uploadTimestamp = thumbnail?.asPointer()?.uploadTimestamp ?: 0,
|
||||
caption = thumbnail?.asPointer()?.caption?.orElse(null),
|
||||
stickerLocator = null,
|
||||
blurHash = null
|
||||
blurHash = null,
|
||||
uuid = thumbnail?.asPointer()?.uuid
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* An attachment that represents where an attachment used to be. Useful when you need to know that
|
||||
@@ -35,7 +36,8 @@ class TombstoneAttachment : Attachment {
|
||||
stickerLocator = null,
|
||||
blurHash = null,
|
||||
audioHash = null,
|
||||
transformProperties = null
|
||||
transformProperties = null,
|
||||
uuid = null
|
||||
)
|
||||
|
||||
constructor(
|
||||
@@ -49,7 +51,8 @@ class TombstoneAttachment : Attachment {
|
||||
voiceNote: Boolean = false,
|
||||
borderless: Boolean = false,
|
||||
gif: Boolean = false,
|
||||
quote: Boolean
|
||||
quote: Boolean,
|
||||
uuid: UUID?
|
||||
) : super(
|
||||
contentType = contentType ?: "",
|
||||
quote = quote,
|
||||
@@ -73,7 +76,8 @@ class TombstoneAttachment : Attachment {
|
||||
stickerLocator = null,
|
||||
blurHash = BlurHash.parseOrNull(blurHash),
|
||||
audioHash = null,
|
||||
transformProperties = null
|
||||
transformProperties = null,
|
||||
uuid = uuid
|
||||
)
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel)
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import java.util.Objects
|
||||
import java.util.UUID
|
||||
|
||||
class UriAttachment : Attachment {
|
||||
|
||||
@@ -46,6 +47,7 @@ class UriAttachment : Attachment {
|
||||
transformProperties = transformProperties
|
||||
)
|
||||
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
dataUri: Uri,
|
||||
contentType: String,
|
||||
@@ -63,7 +65,8 @@ class UriAttachment : Attachment {
|
||||
stickerLocator: StickerLocator?,
|
||||
blurHash: BlurHash?,
|
||||
audioHash: AudioHash?,
|
||||
transformProperties: TransformProperties?
|
||||
transformProperties: TransformProperties?,
|
||||
uuid: UUID? = UUID.randomUUID()
|
||||
) : super(
|
||||
contentType = contentType,
|
||||
transferState = transferState,
|
||||
@@ -87,7 +90,8 @@ class UriAttachment : Attachment {
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
audioHash = audioHash,
|
||||
transformProperties = transformProperties
|
||||
transformProperties = transformProperties,
|
||||
uuid = uuid
|
||||
) {
|
||||
uri = Objects.requireNonNull(dataUri)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.annotation.RequiresApi
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioHandler
|
||||
|
||||
internal const val TAG = "BluetoothVoiceNoteUtil"
|
||||
@@ -34,7 +34,7 @@ sealed interface BluetoothVoiceNoteUtil {
|
||||
@RequiresApi(31)
|
||||
private class BluetoothVoiceNoteUtil31(val listener: (Boolean) -> Unit) : BluetoothVoiceNoteUtil {
|
||||
override fun connectBluetoothScoConnection() {
|
||||
val audioManager = ApplicationDependencies.getAndroidCallAudioManager()
|
||||
val audioManager = AppDependencies.androidCallAudioManager
|
||||
val device: AudioDeviceInfo? = audioManager.connectedBluetoothDevice
|
||||
if (device != null) {
|
||||
val result: Boolean = audioManager.setCommunicationDevice(device)
|
||||
@@ -53,7 +53,7 @@ private class BluetoothVoiceNoteUtil31(val listener: (Boolean) -> Unit) : Blueto
|
||||
|
||||
override fun disconnectBluetoothScoConnection() {
|
||||
Log.d(TAG, "Clearing call manager communication device.")
|
||||
ApplicationDependencies.getAndroidCallAudioManager().clearCommunicationDevice()
|
||||
AppDependencies.androidCallAudioManager.clearCommunicationDevice()
|
||||
}
|
||||
|
||||
override fun destroy() = Unit
|
||||
|
||||
@@ -16,7 +16,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.safeUnregisterReceiver
|
||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioHandler
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -49,7 +49,7 @@ class SignalBluetoothManager(
|
||||
private var bluetoothHeadset: BluetoothHeadset? = null
|
||||
private var scoConnectionAttempts = 0
|
||||
|
||||
private val androidAudioManager = ApplicationDependencies.getAndroidCallAudioManager()
|
||||
private val androidAudioManager = AppDependencies.androidCallAudioManager
|
||||
private val bluetoothListener = BluetoothServiceListener()
|
||||
private var bluetoothReceiver: BluetoothHeadsetBroadcastReceiver? = null
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.avatar.fallback
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.Px
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.util.NameUtil
|
||||
|
||||
/**
|
||||
* Specifies what kind of avatar should be generated for a given recipient.
|
||||
*/
|
||||
sealed interface FallbackAvatar {
|
||||
|
||||
val color: AvatarColor
|
||||
|
||||
/**
|
||||
* Transparent avatar
|
||||
*/
|
||||
data object Transparent : FallbackAvatar {
|
||||
override val color: AvatarColor = AvatarColor.UNKNOWN
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated avatars utilize the initials of the given recipient
|
||||
*/
|
||||
data class Text(val content: String, override val color: AvatarColor) : FallbackAvatar {
|
||||
init {
|
||||
check(content.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback avatars that are backed by resources.
|
||||
*/
|
||||
sealed interface Resource : FallbackAvatar {
|
||||
|
||||
@DrawableRes
|
||||
fun getIconBySize(size: Size): Int
|
||||
|
||||
/**
|
||||
* Local user
|
||||
*/
|
||||
data class Local(override val color: AvatarColor) : Resource {
|
||||
override fun getIconBySize(size: Size): Int {
|
||||
return when (size) {
|
||||
Size.SMALL -> R.drawable.symbol_note_compact_16
|
||||
Size.MEDIUM -> R.drawable.symbol_note_24
|
||||
Size.LARGE -> R.drawable.symbol_note_display_bold_40
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual user without a display name.
|
||||
*/
|
||||
data class Person(override val color: AvatarColor) : Resource {
|
||||
override fun getIconBySize(size: Size): Int {
|
||||
return when (size) {
|
||||
Size.SMALL -> R.drawable.symbol_person_compact_16
|
||||
Size.MEDIUM -> R.drawable.symbol_person_24
|
||||
Size.LARGE -> R.drawable.symbol_person_display_bold_40
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A group
|
||||
*/
|
||||
data class Group(override val color: AvatarColor) : Resource {
|
||||
override fun getIconBySize(size: Size): Int {
|
||||
return when (size) {
|
||||
Size.SMALL -> R.drawable.symbol_group_compact_16
|
||||
Size.MEDIUM -> R.drawable.symbol_group_24
|
||||
Size.LARGE -> R.drawable.symbol_group_display_bold_40
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Story distribution lists
|
||||
*/
|
||||
data class DistributionList(override val color: AvatarColor) : Resource {
|
||||
override fun getIconBySize(size: Size): Int {
|
||||
return when (size) {
|
||||
Size.SMALL -> R.drawable.symbol_stories_compact_16
|
||||
Size.MEDIUM -> R.drawable.symbol_stories_24
|
||||
Size.LARGE -> R.drawable.symbol_stories_display_bold_40
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Links
|
||||
*/
|
||||
data class CallLink(override val color: AvatarColor) : Resource {
|
||||
override fun getIconBySize(size: Size): Int {
|
||||
return when (size) {
|
||||
Size.SMALL -> R.drawable.symbol_video_compact_16
|
||||
Size.MEDIUM -> R.drawable.symbol_video_24
|
||||
Size.LARGE -> R.drawable.symbol_video_display_bold_40
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Size {
|
||||
/**
|
||||
* Smaller than 32dp
|
||||
*/
|
||||
SMALL,
|
||||
|
||||
/**
|
||||
* 32dp and larger
|
||||
*/
|
||||
MEDIUM,
|
||||
|
||||
/**
|
||||
* 80dp and larger
|
||||
*/
|
||||
LARGE
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ICON_TO_BACKGROUND_SCALE = 0.625
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun forTextOrDefault(text: String, avatarColor: AvatarColor, default: FallbackAvatar = Resource.Person(avatarColor)): FallbackAvatar {
|
||||
val abbreviation = NameUtil.getAbbreviation(text)
|
||||
return if (abbreviation != null) {
|
||||
Text(abbreviation, avatarColor)
|
||||
} else {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
fun getSizeByPx(@Px px: Int): Size {
|
||||
return getSizeByDp(DimensionUnit.PIXELS.toDp(px.toFloat()).dp)
|
||||
}
|
||||
|
||||
fun getSizeByDp(dp: Dp): Size {
|
||||
val rawDp = dp.value
|
||||
return when {
|
||||
rawDp >= 80.0 -> Size.LARGE
|
||||
rawDp < 32.0 -> Size.SMALL
|
||||
else -> Size.MEDIUM
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.avatar.fallback
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.RelativeCornerSize
|
||||
import com.google.android.material.shape.RoundedCornerTreatment
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import org.thoughtcrime.securesms.avatar.Avatar
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.avatar.TextAvatarDrawable
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
|
||||
|
||||
class FallbackAvatarDrawable(
|
||||
private val context: Context,
|
||||
private val fallbackAvatar: FallbackAvatar
|
||||
) : MaterialShapeDrawable() {
|
||||
|
||||
private val avatarColorPair: AvatarColorPair = AvatarColorPair.create(context, fallbackAvatar.color)
|
||||
private var avatarSize: FallbackAvatar.Size = FallbackAvatar.Size.SMALL
|
||||
private var icon: Drawable? = null
|
||||
|
||||
init {
|
||||
fillColor = ColorStateList.valueOf(avatarColorPair.backgroundColor)
|
||||
}
|
||||
|
||||
fun circleCrop(): FallbackAvatarDrawable {
|
||||
shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
.setAllCorners(RoundedCornerTreatment())
|
||||
.setAllCornerSizes(RelativeCornerSize(0.5f))
|
||||
.build()
|
||||
return this
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
super.onBoundsChange(bounds)
|
||||
|
||||
avatarSize = FallbackAvatar.getSizeByPx(bounds.width())
|
||||
icon = when (fallbackAvatar) {
|
||||
is FallbackAvatar.Resource -> {
|
||||
val resourceIcon = ContextCompat.getDrawable(context, fallbackAvatar.getIconBySize(avatarSize))!!
|
||||
|
||||
val iconBounds = Rect(bounds)
|
||||
iconBounds.inset(
|
||||
((bounds.width() - (bounds.width() * FallbackAvatar.ICON_TO_BACKGROUND_SCALE)) / 2f).toInt(),
|
||||
((bounds.height() - (bounds.height() * FallbackAvatar.ICON_TO_BACKGROUND_SCALE)) / 2f).toInt()
|
||||
)
|
||||
|
||||
resourceIcon.bounds = iconBounds
|
||||
resourceIcon
|
||||
}
|
||||
|
||||
is FallbackAvatar.Text -> TextAvatarDrawable(
|
||||
context = context,
|
||||
avatar = Avatar.Text(
|
||||
fallbackAvatar.content,
|
||||
Avatars.ColorPair(avatarColorPair.backgroundColor, avatarColorPair.foregroundColor, ""),
|
||||
Avatar.DatabaseId.DoNotPersist
|
||||
),
|
||||
size = bounds.width()
|
||||
)
|
||||
|
||||
FallbackAvatar.Transparent -> null
|
||||
}
|
||||
|
||||
icon?.alpha = alpha
|
||||
icon?.colorFilter = SimpleColorFilter(avatarColorPair.foregroundColor)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (icon == null) return
|
||||
|
||||
super.draw(canvas)
|
||||
icon?.draw(canvas)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
super.setAlpha(alpha)
|
||||
icon?.alpha = alpha
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.avatar.fallback
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.avatar.AvatarRenderer
|
||||
import org.thoughtcrime.securesms.avatar.Avatars
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
|
||||
|
||||
@Composable
|
||||
fun FallbackAvatarImage(
|
||||
fallbackAvatar: FallbackAvatar,
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = CircleShape
|
||||
) {
|
||||
if (fallbackAvatar is FallbackAvatar.Transparent) {
|
||||
Box(modifier = modifier)
|
||||
return
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val colorPair = remember(fallbackAvatar) {
|
||||
AvatarColorPair.create(context, fallbackAvatar.color)
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.background(Color(colorPair.backgroundColor), shape)
|
||||
) {
|
||||
when (fallbackAvatar) {
|
||||
is FallbackAvatar.Resource -> {
|
||||
val size = remember(maxWidth) {
|
||||
FallbackAvatar.getSizeByDp(maxWidth)
|
||||
}
|
||||
|
||||
val padding = remember(maxWidth) {
|
||||
((maxWidth.value - (maxWidth.value * FallbackAvatar.ICON_TO_BACKGROUND_SCALE)) / 2).dp
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(fallbackAvatar.getIconBySize(size)),
|
||||
contentDescription = null,
|
||||
tint = Color(colorPair.foregroundColor),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
)
|
||||
}
|
||||
|
||||
is FallbackAvatar.Text -> {
|
||||
val size = DimensionUnit.DP.toPixels(maxWidth.value) * 0.8f
|
||||
val textSize = DimensionUnit.PIXELS.toDp(Avatars.getTextSizeForLength(context, fallbackAvatar.content, size, size))
|
||||
|
||||
// TODO [alex] -- Handle emoji
|
||||
|
||||
Text(
|
||||
text = fallbackAvatar.content,
|
||||
color = Color(colorPair.foregroundColor),
|
||||
fontSize = TextUnit(textSize, TextUnitType.Sp),
|
||||
fontFamily = FontFamily(AvatarRenderer.getTypeface(context))
|
||||
)
|
||||
}
|
||||
FallbackAvatar.Transparent -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
fun FallbackAvatarImagePreview() {
|
||||
Previews.Preview {
|
||||
Column {
|
||||
Text(text = "Compose - Large")
|
||||
FallbackAvatarImage(
|
||||
fallbackAvatar = FallbackAvatar.Text("AE", AvatarColor.A100),
|
||||
modifier = Modifier.size(160.dp)
|
||||
)
|
||||
Text(text = "Compose - Medium")
|
||||
FallbackAvatarImage(
|
||||
fallbackAvatar = FallbackAvatar.Text("AE", AvatarColor.A100),
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
Text(text = "Compose - Small")
|
||||
FallbackAvatarImage(
|
||||
fallbackAvatar = FallbackAvatar.Text("AE", AvatarColor.A100),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ 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
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -243,18 +242,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openGallery() {
|
||||
Permissions.with(this)
|
||||
.request(*PermissionCompat.forImages())
|
||||
.ifNecessary()
|
||||
.onAllGranted {
|
||||
val intent = AvatarSelectionActivity.getIntentForGallery(requireContext())
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
||||
}
|
||||
.onAnyDenied {
|
||||
Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__viewing_your_gallery_requires_the_storage_permission, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
.execute()
|
||||
val intent = AvatarSelectionActivity.getIntentForGallery(requireContext())
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
|
||||
@@ -87,8 +87,8 @@ class AvatarView @JvmOverloads constructor(
|
||||
avatar.setRecipient(recipient)
|
||||
}
|
||||
|
||||
fun setFallbackPhotoProvider(fallbackPhotoProvider: Recipient.FallbackPhotoProvider) {
|
||||
avatar.setFallbackPhotoProvider(fallbackPhotoProvider)
|
||||
fun setFallbackAvatarProvider(fallbackAvatarProvider: AvatarImageView.FallbackAvatarProvider?) {
|
||||
avatar.setFallbackAvatarProvider(fallbackAvatarProvider)
|
||||
}
|
||||
|
||||
fun disableQuickContact() {
|
||||
|
||||
@@ -28,7 +28,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
|
||||
import org.thoughtcrime.securesms.restore.restorelocalbackup.PassphraseAsYouTypeFormatter;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -170,7 +170,7 @@ public class BackupDialog {
|
||||
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
positiveButton.setEnabled(false);
|
||||
|
||||
RestoreBackupFragment.PassphraseAsYouTypeFormatter formatter = new RestoreBackupFragment.PassphraseAsYouTypeFormatter();
|
||||
PassphraseAsYouTypeFormatter formatter = new PassphraseAsYouTypeFormatter();
|
||||
|
||||
prompt.addTextChangedListener(new AfterTextChanged(editable -> {
|
||||
formatter.afterTextChanged(editable);
|
||||
|
||||
@@ -45,7 +45,7 @@ import org.thoughtcrime.securesms.database.SessionTable;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyTable;
|
||||
import org.thoughtcrime.securesms.database.StickerTable;
|
||||
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
@@ -235,7 +235,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
|
||||
count += TextSecurePreferences.getPreferencesToSaveToBackupCount(context);
|
||||
|
||||
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication())
|
||||
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(AppDependencies.getApplication())
|
||||
.getDataSet();
|
||||
for (String key : SignalStore.getKeysToIncludeInBackup()) {
|
||||
if (dataSet.containsKey(key)) {
|
||||
@@ -536,7 +536,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
long estimatedCount,
|
||||
BackupCancellationSignal cancellationSignal) throws IOException
|
||||
{
|
||||
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication())
|
||||
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(AppDependencies.getApplication())
|
||||
.getDataSet();
|
||||
|
||||
for (String key : keysToIncludeInBackup) {
|
||||
|
||||
@@ -31,13 +31,12 @@ import org.thoughtcrime.securesms.database.EmojiSearchTable;
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase;
|
||||
import org.thoughtcrime.securesms.database.SearchTable;
|
||||
import org.thoughtcrime.securesms.database.StickerTable;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -99,7 +98,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).getSqlCipherDatabase();
|
||||
SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(AppDependencies.getApplication()).getSqlCipherDatabase();
|
||||
|
||||
db.setForeignKeyConstraintsEnabled(false);
|
||||
db.beginTransaction();
|
||||
@@ -288,7 +287,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).writeDataSet(dataSet, Collections.emptyList());
|
||||
KeyValueDatabase.getInstance(AppDependencies.getApplication()).writeDataSet(dataSet, Collections.emptyList());
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
|
||||
@@ -5,20 +5,28 @@
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.EventTimer
|
||||
import org.signal.core.util.fullWalCheckpoint
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.messagebackup.MessageBackup
|
||||
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
|
||||
import org.signal.libsignal.messagebackup.MessageBackupKey
|
||||
import org.signal.libsignal.protocol.ServiceId.Aci
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getThumbnailMediaName
|
||||
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
|
||||
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
|
||||
@@ -26,18 +34,29 @@ import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.processor.StickerBackupProcessor
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
|
||||
import org.thoughtcrime.securesms.database.DistributionListTables
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
|
||||
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.toMillis
|
||||
import org.whispersystems.signalservice.api.NetworkResult
|
||||
import org.whispersystems.signalservice.api.StatusCodeErrorAction
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
|
||||
@@ -53,84 +72,181 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.Pro
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
|
||||
import java.io.ByteArrayOutputStream
|
||||
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
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
object BackupRepository {
|
||||
|
||||
private val TAG = Log.tag(BackupRepository::class.java)
|
||||
private const val VERSION = 1L
|
||||
private const val MAIN_DB_SNAPSHOT_NAME = "signal-snapshot.db"
|
||||
private const val KEYVALUE_DB_SNAPSHOT_NAME = "key-value-snapshot.db"
|
||||
|
||||
private val resetInitializedStateErrorAction: StatusCodeErrorAction = { error ->
|
||||
when (error.code) {
|
||||
401 -> {
|
||||
Log.i(TAG, "Resetting initialized state due to 401.")
|
||||
SignalStore.backup().backupsInitialized = false
|
||||
SignalStore.backup.backupsInitialized = false
|
||||
}
|
||||
|
||||
403 -> {
|
||||
Log.i(TAG, "Bad auth credential. Clearing stored credentials.")
|
||||
SignalStore.backup().clearAllCredentials()
|
||||
SignalStore.backup.clearAllCredentials()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun turnOffAndDeleteBackup() {
|
||||
RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
SignalStore.backup.areBackupsEnabled = false
|
||||
SignalStore.backup.backupTier = null
|
||||
}
|
||||
|
||||
private fun createSignalDatabaseSnapshot(): SignalDatabase {
|
||||
// Need to do a WAL checkpoint to ensure that the database file we're copying has all pending writes
|
||||
if (!SignalDatabase.rawDatabase.fullWalCheckpoint()) {
|
||||
Log.w(TAG, "Failed to checkpoint WAL for main database! Not guaranteed to be using the most recent data.")
|
||||
}
|
||||
|
||||
// We make a copy of the database within a transaction to ensure that no writes occur while we're copying the file
|
||||
return SignalDatabase.rawDatabase.withinTransaction {
|
||||
val context = AppDependencies.application
|
||||
|
||||
val existingDbFile = context.getDatabasePath(SignalDatabase.DATABASE_NAME)
|
||||
val targetFile = File(existingDbFile.parentFile, MAIN_DB_SNAPSHOT_NAME)
|
||||
|
||||
try {
|
||||
existingDbFile.copyTo(targetFile, overwrite = true)
|
||||
} catch (e: IOException) {
|
||||
// TODO [backup] Gracefully handle this error
|
||||
throw IllegalStateException("Failed to copy database file!", e)
|
||||
}
|
||||
|
||||
SignalDatabase(
|
||||
context = context,
|
||||
databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context),
|
||||
attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||
name = MAIN_DB_SNAPSHOT_NAME
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSignalStoreSnapshot(): SignalStore {
|
||||
val context = AppDependencies.application
|
||||
|
||||
// Need to do a WAL checkpoint to ensure that the database file we're copying has all pending writes
|
||||
if (!KeyValueDatabase.getInstance(context).writableDatabase.fullWalCheckpoint()) {
|
||||
Log.w(TAG, "Failed to checkpoint WAL for KeyValueDatabase! Not guaranteed to be using the most recent data.")
|
||||
}
|
||||
|
||||
// We make a copy of the database within a transaction to ensure that no writes occur while we're copying the file
|
||||
return KeyValueDatabase.getInstance(context).writableDatabase.withinTransaction {
|
||||
val existingDbFile = context.getDatabasePath(KeyValueDatabase.DATABASE_NAME)
|
||||
val targetFile = File(existingDbFile.parentFile, KEYVALUE_DB_SNAPSHOT_NAME)
|
||||
|
||||
try {
|
||||
existingDbFile.copyTo(targetFile, overwrite = true)
|
||||
} catch (e: IOException) {
|
||||
// TODO [backup] Gracefully handle this error
|
||||
throw IllegalStateException("Failed to copy database file!", e)
|
||||
}
|
||||
|
||||
val db = KeyValueDatabase.createWithName(context, KEYVALUE_DB_SNAPSHOT_NAME)
|
||||
SignalStore(KeyValueStore(db))
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteDatabaseSnapshot() {
|
||||
val targetFile = AppDependencies.application.getDatabasePath(MAIN_DB_SNAPSHOT_NAME)
|
||||
if (!targetFile.delete()) {
|
||||
Log.w(TAG, "Failed to delete main database snapshot!")
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteSignalStoreSnapshot() {
|
||||
val targetFile = AppDependencies.application.getDatabasePath(KEYVALUE_DB_SNAPSHOT_NAME)
|
||||
if (!targetFile.delete()) {
|
||||
Log.w(TAG, "Failed to delete key value database snapshot!")
|
||||
}
|
||||
}
|
||||
|
||||
fun export(outputStream: OutputStream, append: (ByteArray) -> Unit, plaintext: Boolean = false) {
|
||||
val eventTimer = EventTimer()
|
||||
val writer: BackupExportWriter = if (plaintext) {
|
||||
PlainTextBackupWriter(outputStream)
|
||||
} else {
|
||||
EncryptedBackupWriter(
|
||||
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
|
||||
aci = SignalStore.account().aci!!,
|
||||
outputStream = outputStream,
|
||||
append = append
|
||||
)
|
||||
}
|
||||
val dbSnapshot: SignalDatabase = createSignalDatabaseSnapshot()
|
||||
val signalStoreSnapshot: SignalStore = createSignalStoreSnapshot()
|
||||
|
||||
val exportState = ExportState(backupTime = System.currentTimeMillis(), allowMediaBackup = true)
|
||||
|
||||
writer.use {
|
||||
writer.write(
|
||||
BackupInfo(
|
||||
version = VERSION,
|
||||
backupTimeMs = exportState.backupTime
|
||||
try {
|
||||
val writer: BackupExportWriter = if (plaintext) {
|
||||
PlainTextBackupWriter(outputStream)
|
||||
} else {
|
||||
EncryptedBackupWriter(
|
||||
key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(),
|
||||
aci = SignalStore.account.aci!!,
|
||||
outputStream = outputStream,
|
||||
append = append
|
||||
)
|
||||
)
|
||||
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
|
||||
// writes from other threads are blocked. This is something to think more about.
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
AccountDataProcessor.export {
|
||||
writer.write(it)
|
||||
eventTimer.emit("account")
|
||||
}
|
||||
}
|
||||
|
||||
RecipientBackupProcessor.export(exportState) {
|
||||
writer.write(it)
|
||||
eventTimer.emit("recipient")
|
||||
}
|
||||
val exportState = ExportState(backupTime = System.currentTimeMillis(), allowMediaBackup = SignalStore.backup.backsUpMedia)
|
||||
|
||||
ChatBackupProcessor.export(exportState) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("thread")
|
||||
}
|
||||
writer.use {
|
||||
writer.write(
|
||||
BackupInfo(
|
||||
version = VERSION,
|
||||
backupTimeMs = exportState.backupTime
|
||||
)
|
||||
)
|
||||
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
|
||||
// writes from other threads are blocked. This is something to think more about.
|
||||
dbSnapshot.rawWritableDatabase.withinTransaction {
|
||||
AccountDataProcessor.export(dbSnapshot, signalStoreSnapshot) {
|
||||
writer.write(it)
|
||||
eventTimer.emit("account")
|
||||
}
|
||||
|
||||
AdHocCallBackupProcessor.export { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("call")
|
||||
}
|
||||
RecipientBackupProcessor.export(dbSnapshot, signalStoreSnapshot, exportState) {
|
||||
writer.write(it)
|
||||
eventTimer.emit("recipient")
|
||||
}
|
||||
|
||||
ChatItemBackupProcessor.export(exportState) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("message")
|
||||
ChatBackupProcessor.export(dbSnapshot, exportState) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("thread")
|
||||
}
|
||||
|
||||
AdHocCallBackupProcessor.export(dbSnapshot) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("call")
|
||||
}
|
||||
|
||||
StickerBackupProcessor.export(dbSnapshot) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("sticker-pack")
|
||||
}
|
||||
|
||||
ChatItemBackupProcessor.export(dbSnapshot, exportState) { frame ->
|
||||
writer.write(frame)
|
||||
eventTimer.emit("message")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "export() ${eventTimer.stop().summary}")
|
||||
Log.d(TAG, "export() ${eventTimer.stop().summary}")
|
||||
} finally {
|
||||
deleteDatabaseSnapshot()
|
||||
deleteSignalStoreSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
fun export(plaintext: Boolean = false): ByteArray {
|
||||
@@ -140,7 +256,7 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun validate(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData): ValidationResult {
|
||||
val masterKey = SignalStore.svr().getOrCreateMasterKey()
|
||||
val masterKey = SignalStore.svr.getOrCreateMasterKey()
|
||||
val key = MessageBackupKey(masterKey.serialize(), Aci.parseFromBinary(selfData.aci.toByteArray()))
|
||||
|
||||
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, inputStreamFactory, length)
|
||||
@@ -149,15 +265,15 @@ object BackupRepository {
|
||||
fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false) {
|
||||
val eventTimer = EventTimer()
|
||||
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
val frameReader = if (plaintext) {
|
||||
PlainTextBackupReader(inputStreamFactory())
|
||||
PlainTextBackupReader(inputStreamFactory(), length)
|
||||
} else {
|
||||
EncryptedBackupReader(
|
||||
key = backupKey,
|
||||
aci = selfData.aci,
|
||||
streamLength = length,
|
||||
length = length,
|
||||
dataStream = inputStreamFactory
|
||||
)
|
||||
}
|
||||
@@ -180,16 +296,21 @@ object BackupRepository {
|
||||
SignalDatabase.threads.clearAllDataForBackupRestore()
|
||||
SignalDatabase.messages.clearAllDataForBackupRestore()
|
||||
SignalDatabase.attachments.clearAllDataForBackupRestore()
|
||||
SignalDatabase.stickers.clearAllDataForBackupRestore()
|
||||
|
||||
// Add back self after clearing data
|
||||
val selfId: RecipientId = SignalDatabase.recipients.getAndPossiblyMerge(selfData.aci, selfData.pni, selfData.e164, pniVerified = true, changeSelf = true)
|
||||
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
|
||||
SignalDatabase.recipients.setProfileSharing(selfId, true)
|
||||
|
||||
// Add back my story after clearing data
|
||||
DistributionListTables.insertInitialDistributionListAtCreationTime(it)
|
||||
|
||||
eventTimer.emit("setup")
|
||||
val backupState = BackupState(backupKey)
|
||||
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
|
||||
|
||||
val totalLength = frameReader.getStreamLength()
|
||||
for (frame in frameReader) {
|
||||
when {
|
||||
frame.account != null -> {
|
||||
@@ -212,6 +333,11 @@ object BackupRepository {
|
||||
eventTimer.emit("call")
|
||||
}
|
||||
|
||||
frame.stickerPack != null -> {
|
||||
StickerBackupProcessor.import(frame.stickerPack)
|
||||
eventTimer.emit("sticker-pack")
|
||||
}
|
||||
|
||||
frame.chatItem != null -> {
|
||||
chatItemInserter.insert(frame.chatItem)
|
||||
eventTimer.emit("chatItem")
|
||||
@@ -220,6 +346,7 @@ object BackupRepository {
|
||||
|
||||
else -> Log.w(TAG, "Unrecognized frame")
|
||||
}
|
||||
EventBus.getDefault().post(RestoreV2Event(RestoreV2Event.Type.PROGRESS_RESTORE, frameReader.getBytesRead(), totalLength))
|
||||
}
|
||||
|
||||
if (chatItemInserter.flush()) {
|
||||
@@ -235,7 +362,7 @@ object BackupRepository {
|
||||
while (groups.hasNext()) {
|
||||
val group = groups.next()
|
||||
if (group.id.isV2) {
|
||||
ApplicationDependencies.getJobManager().add(RequestGroupV2InfoJob(group.id as GroupId.V2))
|
||||
AppDependencies.jobManager.add(RequestGroupV2InfoJob(group.id as GroupId.V2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,8 +370,8 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun listRemoteMediaObjects(limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -253,8 +380,8 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun getRemoteBackupUsedSpace(): NetworkResult<Long?> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -263,12 +390,27 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBackupTier(): NetworkResult<MessageBackupTier> {
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.map { credential ->
|
||||
val zkCredential = api.getZkCredential(backupKey, credential)
|
||||
if (zkCredential.backupLevel == BackupLevel.MEDIA) {
|
||||
MessageBackupTier.PAID
|
||||
} else {
|
||||
MessageBackupTier.FREE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with details about the remote backup state.
|
||||
*/
|
||||
fun getRemoteBackupState(): NetworkResult<BackupMetadata> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -294,8 +436,8 @@ object BackupRepository {
|
||||
* @return True if successful, otherwise false.
|
||||
*/
|
||||
fun uploadBackupFile(backupStream: InputStream, backupStreamLength: Long): Boolean {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -316,8 +458,8 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun downloadBackupFile(destination: File, listener: ProgressListener? = null): Boolean {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -326,17 +468,35 @@ object BackupRepository {
|
||||
.then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
|
||||
.map { pair ->
|
||||
val (cdnCredentials, info) = pair
|
||||
val messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver()
|
||||
val messageReceiver = AppDependencies.signalServiceMessageReceiver
|
||||
messageReceiver.retrieveBackup(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}", destination, listener)
|
||||
} is NetworkResult.Success
|
||||
}
|
||||
|
||||
fun getBackupFileLastModified(): NetworkResult<ZonedDateTime?> {
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
api.getBackupInfo(backupKey, credential)
|
||||
}
|
||||
.then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
|
||||
.then { pair ->
|
||||
val (cdnCredentials, info) = pair
|
||||
val messageReceiver = AppDependencies.signalServiceMessageReceiver
|
||||
NetworkResult.fromFetch {
|
||||
messageReceiver.getCdnLastModifiedTime(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with details about the remote backup state.
|
||||
*/
|
||||
fun debugGetArchivedMediaState(): NetworkResult<List<ArchiveGetMediaItemsResponse.StoredMediaObject>> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -348,8 +508,8 @@ object BackupRepository {
|
||||
* Retrieves an upload spec that can be used to upload attachment media.
|
||||
*/
|
||||
fun getMediaUploadSpec(secretKey: ByteArray? = null): NetworkResult<ResumableUploadSpec> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -361,8 +521,8 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun archiveThumbnail(thumbnailAttachment: Attachment, parentAttachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
val request = thumbnailAttachment.toArchiveMediaRequest(parentAttachment.getThumbnailMediaName(), backupKey)
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
@@ -376,8 +536,8 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<Unit> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -399,8 +559,8 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun archiveMedia(databaseAttachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResult> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -439,8 +599,8 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun deleteArchivedMedia(attachments: List<DatabaseAttachment>): NetworkResult<Unit> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
val mediaToDelete = attachments
|
||||
.filter { it.archiveMediaId != null }
|
||||
@@ -471,8 +631,8 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun deleteAbandonedMediaObjects(mediaObjects: Collection<ArchivedMediaObject>): NetworkResult<Unit> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
val mediaToDelete = mediaObjects
|
||||
.map {
|
||||
@@ -499,8 +659,8 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
fun debugDeleteAllArchivedMedia(): NetworkResult<Unit> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return debugGetArchivedMediaState()
|
||||
.then { archivedMedia ->
|
||||
@@ -536,13 +696,13 @@ object BackupRepository {
|
||||
* Retrieve credentials for reading from the backup cdn.
|
||||
*/
|
||||
fun getCdnReadCredentials(cdnNumber: Int): NetworkResult<GetArchiveCdnCredentialsResponse> {
|
||||
val cached = SignalStore.backup().cdnReadCredentials
|
||||
val cached = SignalStore.backup.cdnReadCredentials
|
||||
if (cached != null) {
|
||||
return NetworkResult.Success(cached)
|
||||
}
|
||||
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
@@ -554,20 +714,41 @@ object BackupRepository {
|
||||
}
|
||||
.also {
|
||||
if (it is NetworkResult.Success) {
|
||||
SignalStore.backup().cdnReadCredentials = it.result
|
||||
SignalStore.backup.cdnReadCredentials = it.result
|
||||
}
|
||||
}
|
||||
.also { Log.i(TAG, "getCdnReadCredentialsResult: $it") }
|
||||
}
|
||||
|
||||
fun restoreBackupTier(): MessageBackupTier? {
|
||||
// TODO: more complete error handling
|
||||
try {
|
||||
val lastModified = getBackupFileLastModified().successOrThrow()
|
||||
if (lastModified != null) {
|
||||
SignalStore.backup.lastBackupTime = lastModified.toMillis()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Could not check for backup file.", e)
|
||||
SignalStore.backup.backupTier = null
|
||||
return null
|
||||
}
|
||||
SignalStore.backup.backupTier = try {
|
||||
getBackupTier().successOrThrow()
|
||||
} catch (e: Exception) {
|
||||
Log.i(TAG, "Could not retrieve backup tier.", e)
|
||||
null
|
||||
}
|
||||
return SignalStore.backup.backupTier
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves backupDir and mediaDir, preferring cached value if available.
|
||||
*
|
||||
* These will only ever change if the backup expires.
|
||||
*/
|
||||
fun getCdnBackupDirectories(): NetworkResult<BackupDirectories> {
|
||||
val cachedBackupDirectory = SignalStore.backup().cachedBackupDirectory
|
||||
val cachedBackupMediaDirectory = SignalStore.backup().cachedBackupMediaDirectory
|
||||
val cachedBackupDirectory = SignalStore.backup.cachedBackupDirectory
|
||||
val cachedBackupMediaDirectory = SignalStore.backup.cachedBackupMediaDirectory
|
||||
|
||||
if (cachedBackupDirectory != null && cachedBackupMediaDirectory != null) {
|
||||
return NetworkResult.Success(
|
||||
@@ -578,39 +759,115 @@ object BackupRepository {
|
||||
)
|
||||
}
|
||||
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
val backupKey = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey()
|
||||
|
||||
return initBackupAndFetchAuth(backupKey)
|
||||
.then { credential ->
|
||||
api.getBackupInfo(backupKey, credential).map {
|
||||
SignalStore.backup().usedBackupMediaSpace = it.usedSpace ?: 0L
|
||||
SignalStore.backup.usedBackupMediaSpace = it.usedSpace ?: 0L
|
||||
BackupDirectories(it.backupDir!!, it.mediaDir!!)
|
||||
}
|
||||
}
|
||||
.also {
|
||||
if (it is NetworkResult.Success) {
|
||||
SignalStore.backup().cachedBackupDirectory = it.result.backupDir
|
||||
SignalStore.backup().cachedBackupMediaDirectory = it.result.mediaDir
|
||||
SignalStore.backup.cachedBackupDirectory = it.result.backupDir
|
||||
SignalStore.backup.cachedBackupMediaDirectory = it.result.mediaDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAvailableBackupsTypes(availableBackupTiers: List<MessageBackupTier>): List<MessageBackupsType> {
|
||||
return availableBackupTiers.map { getBackupsType(it) }
|
||||
}
|
||||
|
||||
suspend fun getBackupsType(tier: MessageBackupTier): MessageBackupsType {
|
||||
val backupCurrency = SignalStore.donations.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
return when (tier) {
|
||||
MessageBackupTier.FREE -> getFreeType(backupCurrency)
|
||||
MessageBackupTier.PAID -> getPaidType(backupCurrency)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFreeType(currency: Currency): MessageBackupsType {
|
||||
return MessageBackupsType(
|
||||
tier = MessageBackupTier.FREE,
|
||||
pricePerMonth = FiatMoney(BigDecimal.ZERO, currency),
|
||||
title = "Text + 30 days of media", // TODO [message-backups] Finalize text (does this come from server?)
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Last 30 days of media" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getPaidType(currency: Currency): MessageBackupsType {
|
||||
val serviceResponse = withContext(Dispatchers.IO) {
|
||||
AppDependencies
|
||||
.donationsService
|
||||
.getDonationsConfiguration(Locale.getDefault())
|
||||
}
|
||||
|
||||
if (serviceResponse.result.isEmpty) {
|
||||
if (serviceResponse.applicationError.isPresent) {
|
||||
throw serviceResponse.applicationError.get()
|
||||
}
|
||||
|
||||
if (serviceResponse.executionError.isPresent) {
|
||||
throw serviceResponse.executionError.get()
|
||||
}
|
||||
|
||||
error("Unhandled error occurred while downloading configuration.")
|
||||
}
|
||||
|
||||
val config = serviceResponse.result.get()
|
||||
|
||||
return MessageBackupsType(
|
||||
tier = MessageBackupTier.PAID,
|
||||
pricePerMonth = FiatMoney(config.currencies[currency.currencyCode.lowercase()]!!.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]!!, currency),
|
||||
title = "Text + All your media", // TODO [message-backups] Finalize text (does this come from server?)
|
||||
features = persistentListOf(
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_album_compact_bold_16,
|
||||
label = "Full media backup" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
|
||||
label = "1TB of storage (~250K photos)" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
),
|
||||
MessageBackupsTypeFeature(
|
||||
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
|
||||
label = "Thanks for supporting Signal!" // TODO [message-backups] Finalize text (does this come from server?)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the backupId has been reserved and that your public key has been set, while also returning an auth credential.
|
||||
* Should be the basis of all backup operations.
|
||||
*/
|
||||
private fun initBackupAndFetchAuth(backupKey: BackupKey): NetworkResult<ArchiveServiceCredential> {
|
||||
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
|
||||
val api = AppDependencies.signalServiceAccountManager.archiveApi
|
||||
|
||||
return if (SignalStore.backup().backupsInitialized) {
|
||||
return if (SignalStore.backup.backupsInitialized) {
|
||||
getAuthCredential().runOnStatusCodeError(resetInitializedStateErrorAction)
|
||||
} else {
|
||||
return api
|
||||
.triggerBackupIdReservation(backupKey)
|
||||
.then { getAuthCredential() }
|
||||
.then { credential -> api.setPublicKey(backupKey, credential).map { credential } }
|
||||
.runIfSuccessful { SignalStore.backup().backupsInitialized = true }
|
||||
.runIfSuccessful { SignalStore.backup.backupsInitialized = true }
|
||||
.runOnStatusCodeError(resetInitializedStateErrorAction)
|
||||
}
|
||||
}
|
||||
@@ -621,7 +878,7 @@ object BackupRepository {
|
||||
private fun getAuthCredential(): NetworkResult<ArchiveServiceCredential> {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
val credential = SignalStore.backup().credentialsByDay.getForCurrentTime(currentTime.milliseconds)
|
||||
val credential = SignalStore.backup.credentialsByDay.getForCurrentTime(currentTime.milliseconds)
|
||||
|
||||
if (credential != null) {
|
||||
return NetworkResult.Success(credential)
|
||||
@@ -629,10 +886,10 @@ object BackupRepository {
|
||||
|
||||
Log.w(TAG, "No credentials found for today, need to fetch new ones! This shouldn't happen under normal circumstances. We should ensure the routine fetch is running properly.")
|
||||
|
||||
return ApplicationDependencies.getSignalServiceAccountManager().archiveApi.getServiceCredentials(currentTime).map { result ->
|
||||
SignalStore.backup().addCredentials(result.credentials.toList())
|
||||
SignalStore.backup().clearCredentialsOlderThan(currentTime)
|
||||
SignalStore.backup().credentialsByDay.getForCurrentTime(currentTime.milliseconds)!!
|
||||
return AppDependencies.signalServiceAccountManager.archiveApi.getServiceCredentials(currentTime).map { result ->
|
||||
SignalStore.backup.addCredentials(result.credentials.toList())
|
||||
SignalStore.backup.clearCredentialsOlderThan(currentTime)
|
||||
SignalStore.backup.credentialsByDay.getForCurrentTime(currentTime.milliseconds)!!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,8 +945,3 @@ class BackupMetadata(
|
||||
val usedSpace: Long,
|
||||
val mediaCount: Long
|
||||
)
|
||||
|
||||
enum class MessageBackupTier {
|
||||
FREE,
|
||||
PAID
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RestoreAttachmentThumbnailJob
|
||||
|
||||
/**
|
||||
* Responsible for managing logic around restore prioritization
|
||||
@@ -28,17 +29,24 @@ object BackupRestoreManager {
|
||||
fun prioritizeAttachmentsIfNeeded(messageRecords: List<MessageRecord>) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
synchronized(this) {
|
||||
val restoringAttachments: List<AttachmentId> = messageRecords
|
||||
val restoringAttachments = messageRecords
|
||||
.mapNotNull { (it as? MmsMessageRecord?)?.slideDeck?.slides }
|
||||
.flatten()
|
||||
.mapNotNull { it.asAttachment() as? DatabaseAttachment }
|
||||
.filter { it.transferState == AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS && !reprioritizedAttachments.contains(it.attachmentId) }
|
||||
.map { it.attachmentId }
|
||||
.filter {
|
||||
val needThumbnail = it.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.NEEDS_RESTORE && it.transferState == AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS
|
||||
(needThumbnail || it.thumbnailRestoreState == AttachmentTable.ThumbnailRestoreState.IN_PROGRESS) && !reprioritizedAttachments.contains(it.attachmentId)
|
||||
}
|
||||
.map { it.attachmentId to it.mmsId }
|
||||
.toSet()
|
||||
|
||||
reprioritizedAttachments += restoringAttachments
|
||||
|
||||
if (restoringAttachments.isNotEmpty()) {
|
||||
RestoreAttachmentJob.modifyPriorities(restoringAttachments.toSet(), 1)
|
||||
reprioritizedAttachments += restoringAttachments.map { it.first }
|
||||
val thumbnailJobs = restoringAttachments.map {
|
||||
val (attachmentId, mmsId) = it
|
||||
RestoreAttachmentThumbnailJob(attachmentId = attachmentId, messageId = mmsId, highPriority = true)
|
||||
}
|
||||
if (thumbnailJobs.isNotEmpty()) {
|
||||
AppDependencies.jobManager.addAll(thumbnailJobs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
class BackupV2Event(val type: Type, val count: Long, val estimatedTotalCount: Long) {
|
||||
enum class Type {
|
||||
PROGRESS_MESSAGES, PROGRESS_ATTACHMENTS, FINISHED
|
||||
PROGRESS_MESSAGES,
|
||||
PROGRESS_ATTACHMENTS,
|
||||
FINISHED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
|
||||
/**
|
||||
* Serializable enum value for what we think a user's current backup tier is.
|
||||
*
|
||||
* We should not trust the stored value on its own, we should also verify it
|
||||
* against what the server knows, but it is a useful flag that helps avoid a
|
||||
* network call in some cases.
|
||||
*/
|
||||
enum class MessageBackupTier(val value: Int) {
|
||||
FREE(0),
|
||||
PAID(1);
|
||||
|
||||
companion object Serializer : LongSerializer<MessageBackupTier?> {
|
||||
override fun serialize(data: MessageBackupTier?): Long {
|
||||
return data?.value?.toLong() ?: -1
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): MessageBackupTier? {
|
||||
return entries.firstOrNull { it.value == data.toInt() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
class RestoreV2Event(val type: Type, val count: Long, val estimatedTotalCount: Long) {
|
||||
enum class Type {
|
||||
PROGRESS_DOWNLOAD,
|
||||
PROGRESS_RESTORE,
|
||||
PROGRESS_MEDIA_RESTORE,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
fun getProgress(): Float {
|
||||
if (estimatedTotalCount == 0L) {
|
||||
return 0f
|
||||
}
|
||||
return count.toFloat() / estimatedTotalCount.toFloat()
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.database
|
||||
|
||||
import android.database.Cursor
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
@@ -63,8 +64,8 @@ class BackupCallLinkIterator(private val cursor: Cursor) : Iterator<BackupRecipi
|
||||
return BackupRecipient(
|
||||
id = callLink.recipientId.toLong(),
|
||||
callLink = CallLink(
|
||||
rootKey = callLink.credentials!!.linkKeyBytes.toByteString(),
|
||||
adminKey = callLink.credentials.adminPassBytes?.toByteString(),
|
||||
rootKey = callLink.credentials?.linkKeyBytes?.toByteString() ?: ByteString.EMPTY,
|
||||
adminKey = callLink.credentials?.adminPassBytes?.toByteString(),
|
||||
name = callLink.state.name,
|
||||
expirationMs = callLink.state.expiration.toEpochMilli(),
|
||||
restrictions = callLink.state.restrictions.toBackup()
|
||||
|
||||
@@ -38,7 +38,7 @@ fun CallTable.restoreCallLogFromBackup(call: AdHocCall, backupState: BackupState
|
||||
CallTable.TYPE to CallTable.Type.serialize(CallTable.Type.AD_HOC_CALL),
|
||||
CallTable.DIRECTION to CallTable.Direction.serialize(CallTable.Direction.OUTGOING),
|
||||
CallTable.EVENT to CallTable.Event.serialize(event),
|
||||
CallTable.TIMESTAMP to call.startedCallTimestamp
|
||||
CallTable.TIMESTAMP to call.callTimestamp
|
||||
)
|
||||
|
||||
writableDatabase.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
|
||||
@@ -64,7 +64,7 @@ class CallLogIterator(private val cursor: Cursor) : Iterator<AdHocCall?>, Closea
|
||||
callId = callId,
|
||||
recipientId = cursor.requireLong(CallTable.PEER),
|
||||
state = AdHocCall.State.GENERIC,
|
||||
startedCallTimestamp = cursor.requireLong(CallTable.TIMESTAMP)
|
||||
callTimestamp = cursor.requireLong(CallTable.TIMESTAMP)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ package org.thoughtcrime.securesms.backup.v2.database
|
||||
import android.database.Cursor
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Base64.decode
|
||||
import org.signal.core.util.Base64.decodeOrThrow
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireBlob
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireLongOrNull
|
||||
import org.signal.core.util.requireString
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
@@ -25,7 +25,9 @@ import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.LearnedProfileChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||
@@ -34,6 +36,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SessionSwitchoverChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Sticker
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.StickerMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Text
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
@@ -41,8 +45,10 @@ import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.PaymentTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
|
||||
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
|
||||
@@ -51,21 +57,28 @@ import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.payments.FailureReason
|
||||
import org.thoughtcrime.securesms.payments.State
|
||||
import org.thoughtcrime.securesms.payments.proto.PaymentMetaData
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.api.util.toByteArray
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.util.HashMap
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
|
||||
|
||||
/**
|
||||
* An iterator for chat items with a clever performance twist: rather than do the extra queries one at a time (for reactions,
|
||||
@@ -74,7 +87,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
|
||||
*
|
||||
* All of this complexity is hidden from the user -- they just get a normal iterator interface.
|
||||
*/
|
||||
class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int, private val archiveMedia: Boolean) : Iterator<ChatItem>, Closeable {
|
||||
class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int, private val archiveMedia: Boolean) : Iterator<ChatItem?>, Closeable {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(ChatItemExportIterator::class.java)
|
||||
@@ -88,11 +101,13 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
*/
|
||||
private val buffer: Queue<ChatItem> = LinkedList()
|
||||
|
||||
private val revisionMap: HashMap<Long, ArrayList<ChatItem>> = HashMap()
|
||||
|
||||
override fun hasNext(): Boolean {
|
||||
return buffer.isNotEmpty() || (cursor.count > 0 && !cursor.isLast && !cursor.isAfterLast)
|
||||
}
|
||||
|
||||
override fun next(): ChatItem {
|
||||
override fun next(): ChatItem? {
|
||||
if (buffer.isNotEmpty()) {
|
||||
return buffer.remove()
|
||||
}
|
||||
@@ -117,219 +132,106 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
val builder = record.toBasicChatItemBuilder(groupReceiptsById[id])
|
||||
|
||||
when {
|
||||
record.remoteDeleted -> builder.remoteDeletedMessage = RemoteDeletedMessage()
|
||||
MessageTypes.isJoinedType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.JOINED_SIGNAL))
|
||||
MessageTypes.isIdentityUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_UPDATE))
|
||||
MessageTypes.isIdentityVerified(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_VERIFIED))
|
||||
MessageTypes.isIdentityDefault(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_DEFAULT))
|
||||
record.remoteDeleted -> {
|
||||
builder.remoteDeletedMessage = RemoteDeletedMessage()
|
||||
}
|
||||
MessageTypes.isJoinedType(record.type) -> {
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.JOINED_SIGNAL)
|
||||
}
|
||||
MessageTypes.isIdentityUpdate(record.type) -> {
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.IDENTITY_UPDATE)
|
||||
}
|
||||
MessageTypes.isIdentityVerified(record.type) -> {
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.IDENTITY_VERIFIED)
|
||||
}
|
||||
MessageTypes.isIdentityDefault(record.type) -> {
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.IDENTITY_DEFAULT)
|
||||
}
|
||||
MessageTypes.isChangeNumber(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHANGE_NUMBER))
|
||||
builder.sms = false
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.CHANGE_NUMBER)
|
||||
}
|
||||
MessageTypes.isBoostRequest(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BOOST_REQUEST))
|
||||
builder.sms = false
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.BOOST_REQUEST)
|
||||
}
|
||||
MessageTypes.isEndSessionType(record.type) -> {
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.END_SESSION)
|
||||
}
|
||||
MessageTypes.isChatSessionRefresh(record.type) -> {
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.CHAT_SESSION_REFRESH)
|
||||
}
|
||||
MessageTypes.isBadDecryptType(record.type) -> {
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.BAD_DECRYPT)
|
||||
}
|
||||
MessageTypes.isPaymentsActivated(record.type) -> {
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.PAYMENTS_ACTIVATED)
|
||||
}
|
||||
MessageTypes.isPaymentsRequestToActivate(record.type) -> {
|
||||
builder.updateMessage = simpleUpdate(SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST)
|
||||
}
|
||||
MessageTypes.isEndSessionType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.END_SESSION))
|
||||
MessageTypes.isChatSessionRefresh(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHAT_SESSION_REFRESH))
|
||||
MessageTypes.isBadDecryptType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BAD_DECRYPT))
|
||||
MessageTypes.isPaymentsActivated(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENTS_ACTIVATED))
|
||||
MessageTypes.isPaymentsRequestToActivate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST))
|
||||
MessageTypes.isExpirationTimerUpdate(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate(record.expiresIn.toInt()))
|
||||
builder.expiresInMs = 0
|
||||
}
|
||||
MessageTypes.isProfileChange(record.type) -> {
|
||||
if (record.body == null) continue
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
profileChange = try {
|
||||
val decoded: ByteArray = Base64.decode(record.body!!)
|
||||
val profileChangeDetails = ProfileChangeDetails.ADAPTER.decode(decoded)
|
||||
if (profileChangeDetails.profileNameChange != null) {
|
||||
ProfileChangeChatUpdate(previousName = profileChangeDetails.profileNameChange.previous, newName = profileChangeDetails.profileNameChange.newValue)
|
||||
} else {
|
||||
ProfileChangeChatUpdate()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Profile name change details could not be read", e)
|
||||
ProfileChangeChatUpdate()
|
||||
}
|
||||
)
|
||||
builder.sms = false
|
||||
builder.updateMessage = record.toProfileChangeUpdate()
|
||||
}
|
||||
MessageTypes.isSessionSwitchoverType(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
sessionSwitchover = try {
|
||||
val event = SessionSwitchoverEvent.ADAPTER.decode(decodeOrThrow(record.body!!))
|
||||
SessionSwitchoverChatUpdate(event.e164.e164ToLong()!!)
|
||||
} catch (e: Exception) {
|
||||
SessionSwitchoverChatUpdate()
|
||||
}
|
||||
)
|
||||
builder.updateMessage = record.toSessionSwitchoverUpdate()
|
||||
}
|
||||
MessageTypes.isThreadMergeType(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
threadMerge = try {
|
||||
val event = ThreadMergeEvent.ADAPTER.decode(decodeOrThrow(record.body!!))
|
||||
ThreadMergeChatUpdate(event.previousE164.e164ToLong()!!)
|
||||
} catch (e: Exception) {
|
||||
ThreadMergeChatUpdate()
|
||||
}
|
||||
)
|
||||
builder.updateMessage = record.toThreadMergeUpdate()
|
||||
}
|
||||
MessageTypes.isGroupV2(record.type) && MessageTypes.isGroupUpdate(record.type) -> {
|
||||
val groupChange = record.messageExtras?.gv2UpdateDescription?.groupChangeUpdate
|
||||
if (groupChange != null) {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
groupChange = groupChange
|
||||
)
|
||||
} else if (record.body != null) {
|
||||
try {
|
||||
val decoded: ByteArray = decode(record.body)
|
||||
val context = DecryptedGroupV2Context.ADAPTER.decode(decoded)
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
groupChange = GroupsV2UpdateMessageConverter.translateDecryptedChange(selfIds = SignalStore.account().getServiceIds(), context)
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
builder.updateMessage = record.toGroupUpdate()
|
||||
}
|
||||
MessageTypes.isCallLog(record.type) -> {
|
||||
builder.sms = false
|
||||
val call = calls.getCallByMessageId(record.id)
|
||||
if (call != null) {
|
||||
if (call.type == CallTable.Type.GROUP_CALL) {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
groupCall = GroupCall(
|
||||
callId = record.id,
|
||||
state = when (call.event) {
|
||||
CallTable.Event.MISSED -> GroupCall.State.MISSED
|
||||
CallTable.Event.ONGOING -> GroupCall.State.GENERIC
|
||||
CallTable.Event.ACCEPTED -> GroupCall.State.ACCEPTED
|
||||
CallTable.Event.NOT_ACCEPTED -> GroupCall.State.GENERIC
|
||||
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> GroupCall.State.MISSED_NOTIFICATION_PROFILE
|
||||
CallTable.Event.DELETE -> continue
|
||||
CallTable.Event.GENERIC_GROUP_CALL -> GroupCall.State.GENERIC
|
||||
CallTable.Event.JOINED -> GroupCall.State.JOINED
|
||||
CallTable.Event.RINGING -> GroupCall.State.RINGING
|
||||
CallTable.Event.DECLINED -> GroupCall.State.DECLINED
|
||||
CallTable.Event.OUTGOING_RING -> GroupCall.State.OUTGOING_RING
|
||||
},
|
||||
ringerRecipientId = call.ringerRecipient?.toLong(),
|
||||
startedCallAci = if (call.ringerRecipient != null) SignalDatabase.recipients.getRecord(call.ringerRecipient).aci?.toByteString() else null,
|
||||
startedCallTimestamp = call.timestamp
|
||||
)
|
||||
)
|
||||
} else if (call.type != CallTable.Type.AD_HOC_CALL) {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
callId = call.callId,
|
||||
type = if (call.type == CallTable.Type.VIDEO_CALL) IndividualCall.Type.VIDEO_CALL else IndividualCall.Type.AUDIO_CALL,
|
||||
direction = if (call.direction == CallTable.Direction.INCOMING) IndividualCall.Direction.INCOMING else IndividualCall.Direction.OUTGOING,
|
||||
state = when (call.event) {
|
||||
CallTable.Event.MISSED -> IndividualCall.State.MISSED
|
||||
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> IndividualCall.State.MISSED_NOTIFICATION_PROFILE
|
||||
CallTable.Event.ACCEPTED -> IndividualCall.State.ACCEPTED
|
||||
CallTable.Event.NOT_ACCEPTED -> IndividualCall.State.NOT_ACCEPTED
|
||||
else -> IndividualCall.State.UNKNOWN_STATE
|
||||
},
|
||||
startedCallTimestamp = call.timestamp
|
||||
)
|
||||
)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
builder.updateMessage = record.toCallUpdate()
|
||||
}
|
||||
MessageTypes.isPaymentsNotification(record.type) -> {
|
||||
builder.paymentNotification = record.toPaymentNotificationUpdate()
|
||||
}
|
||||
MessageTypes.isGiftBadge(record.type) -> {
|
||||
builder.giftBadge = record.toGiftBadgeUpdate()
|
||||
}
|
||||
else -> {
|
||||
if (record.body == null && !attachmentsById.containsKey(record.id)) {
|
||||
Log.w(TAG, "Record with ID ${record.id} missing a body and doesn't have attachments. Skipping.")
|
||||
continue
|
||||
}
|
||||
|
||||
val attachments = attachmentsById[record.id]
|
||||
val sticker = attachments?.firstOrNull { dbAttachment ->
|
||||
dbAttachment.isSticker
|
||||
}
|
||||
|
||||
if (sticker?.stickerLocator != null) {
|
||||
builder.stickerMessage = sticker.toStickerMessage(reactionsById[id])
|
||||
} else {
|
||||
when {
|
||||
MessageTypes.isMissedAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.AUDIO_CALL,
|
||||
state = IndividualCall.State.MISSED,
|
||||
direction = IndividualCall.Direction.INCOMING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isMissedVideoCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.VIDEO_CALL,
|
||||
state = IndividualCall.State.MISSED,
|
||||
direction = IndividualCall.Direction.INCOMING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isIncomingAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.AUDIO_CALL,
|
||||
state = IndividualCall.State.ACCEPTED,
|
||||
direction = IndividualCall.Direction.INCOMING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isIncomingVideoCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.VIDEO_CALL,
|
||||
state = IndividualCall.State.ACCEPTED,
|
||||
direction = IndividualCall.Direction.INCOMING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isOutgoingAudioCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.AUDIO_CALL,
|
||||
state = IndividualCall.State.ACCEPTED,
|
||||
direction = IndividualCall.Direction.OUTGOING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isOutgoingVideoCall(record.type) -> {
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.VIDEO_CALL,
|
||||
state = IndividualCall.State.ACCEPTED,
|
||||
direction = IndividualCall.Direction.OUTGOING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isGroupCall(record.type) -> {
|
||||
try {
|
||||
val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(record.body)
|
||||
builder.updateMessage = ChatUpdateMessage(
|
||||
groupCall = GroupCall(
|
||||
state = GroupCall.State.GENERIC,
|
||||
startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(),
|
||||
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
|
||||
endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp
|
||||
)
|
||||
)
|
||||
} catch (exception: java.lang.Exception) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id])
|
||||
}
|
||||
}
|
||||
record.body == null && !attachmentsById.containsKey(record.id) -> {
|
||||
Log.w(TAG, "Record missing a body and doesnt have attachments, skipping")
|
||||
continue
|
||||
}
|
||||
else -> builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id])
|
||||
}
|
||||
|
||||
buffer += builder.build()
|
||||
if (record.latestRevisionId == null) {
|
||||
val previousEdits = revisionMap.remove(record.id)
|
||||
if (previousEdits != null) {
|
||||
builder.revisions = previousEdits
|
||||
}
|
||||
buffer += builder.build()
|
||||
} else {
|
||||
var previousEdits = revisionMap[record.latestRevisionId]
|
||||
if (previousEdits == null) {
|
||||
previousEdits = ArrayList()
|
||||
revisionMap[record.latestRevisionId] = previousEdits
|
||||
}
|
||||
previousEdits += builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
return if (buffer.isNotEmpty()) {
|
||||
buffer.remove()
|
||||
} else {
|
||||
throw NoSuchElementException()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,14 +239,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
cursor.close()
|
||||
}
|
||||
|
||||
private fun String.e164ToLong(): Long? {
|
||||
val fixed = if (this.startsWith("+")) {
|
||||
this.substring(1)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
return fixed.toLongOrNull()
|
||||
private fun simpleUpdate(type: SimpleChatUpdate.Type): ChatUpdateMessage {
|
||||
return ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = type))
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toBasicChatItemBuilder(groupReceipts: List<GroupReceiptTable.GroupReceiptInfo>?): ChatItem.Builder {
|
||||
@@ -357,7 +253,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
expireStartDate = if (record.expireStarted > 0) record.expireStarted else 0
|
||||
expiresInMs = if (record.expiresIn > 0) record.expiresIn else 0
|
||||
revisions = emptyList()
|
||||
sms = !MessageTypes.isSecureType(record.type)
|
||||
sms = record.type.isSmsType()
|
||||
if (MessageTypes.isCallLog(record.type)) {
|
||||
directionless = ChatItem.DirectionlessMessageDetails()
|
||||
} else if (MessageTypes.isOutgoingMessageType(record.type)) {
|
||||
@@ -375,6 +271,220 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toProfileChangeUpdate(): ChatUpdateMessage? {
|
||||
val profileChangeDetails = if (this.messageExtras != null) {
|
||||
this.messageExtras.profileChangeDetails
|
||||
} else {
|
||||
Base64.decodeOrNull(this.body)?.let { ProfileChangeDetails.ADAPTER.decode(it) }
|
||||
}
|
||||
|
||||
return if (profileChangeDetails?.profileNameChange != null) {
|
||||
ChatUpdateMessage(profileChange = ProfileChangeChatUpdate(previousName = profileChangeDetails.profileNameChange.previous, newName = profileChangeDetails.profileNameChange.newValue))
|
||||
} else if (profileChangeDetails?.learnedProfileName != null) {
|
||||
ChatUpdateMessage(learnedProfileChange = LearnedProfileChatUpdate(e164 = profileChangeDetails.learnedProfileName.e164?.e164ToLong(), username = profileChangeDetails.learnedProfileName.username))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toSessionSwitchoverUpdate(): ChatUpdateMessage {
|
||||
if (this.body == null) {
|
||||
return ChatUpdateMessage(sessionSwitchover = SessionSwitchoverChatUpdate())
|
||||
}
|
||||
|
||||
return ChatUpdateMessage(
|
||||
sessionSwitchover = try {
|
||||
val event = SessionSwitchoverEvent.ADAPTER.decode(Base64.decodeOrThrow(this.body))
|
||||
SessionSwitchoverChatUpdate(event.e164.e164ToLong()!!)
|
||||
} catch (e: IOException) {
|
||||
SessionSwitchoverChatUpdate()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toThreadMergeUpdate(): ChatUpdateMessage {
|
||||
if (this.body == null) {
|
||||
return ChatUpdateMessage(threadMerge = ThreadMergeChatUpdate())
|
||||
}
|
||||
|
||||
return ChatUpdateMessage(
|
||||
threadMerge = try {
|
||||
val event = ThreadMergeEvent.ADAPTER.decode(Base64.decodeOrThrow(this.body))
|
||||
ThreadMergeChatUpdate(event.previousE164.e164ToLong()!!)
|
||||
} catch (e: IOException) {
|
||||
ThreadMergeChatUpdate()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toGroupUpdate(): ChatUpdateMessage? {
|
||||
val groupChange = this.messageExtras?.gv2UpdateDescription?.groupChangeUpdate
|
||||
return if (groupChange != null) {
|
||||
ChatUpdateMessage(
|
||||
groupChange = groupChange
|
||||
)
|
||||
} else if (this.body != null) {
|
||||
try {
|
||||
val decoded: ByteArray = Base64.decode(this.body)
|
||||
val context = DecryptedGroupV2Context.ADAPTER.decode(decoded)
|
||||
ChatUpdateMessage(
|
||||
groupChange = GroupsV2UpdateMessageConverter.translateDecryptedChange(selfIds = SignalStore.account.getServiceIds(), context)
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toCallUpdate(): ChatUpdateMessage? {
|
||||
val call = calls.getCallByMessageId(this.id)
|
||||
|
||||
return if (call != null) {
|
||||
call.toCallUpdate()
|
||||
} else {
|
||||
when {
|
||||
MessageTypes.isMissedAudioCall(this.type) -> {
|
||||
ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.AUDIO_CALL,
|
||||
state = IndividualCall.State.MISSED,
|
||||
direction = IndividualCall.Direction.INCOMING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isMissedVideoCall(this.type) -> {
|
||||
ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.VIDEO_CALL,
|
||||
state = IndividualCall.State.MISSED,
|
||||
direction = IndividualCall.Direction.INCOMING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isIncomingAudioCall(this.type) -> {
|
||||
ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.AUDIO_CALL,
|
||||
state = IndividualCall.State.ACCEPTED,
|
||||
direction = IndividualCall.Direction.INCOMING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isIncomingVideoCall(this.type) -> {
|
||||
ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.VIDEO_CALL,
|
||||
state = IndividualCall.State.ACCEPTED,
|
||||
direction = IndividualCall.Direction.INCOMING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isOutgoingAudioCall(this.type) -> {
|
||||
ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.AUDIO_CALL,
|
||||
state = IndividualCall.State.ACCEPTED,
|
||||
direction = IndividualCall.Direction.OUTGOING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isOutgoingVideoCall(this.type) -> {
|
||||
ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
type = IndividualCall.Type.VIDEO_CALL,
|
||||
state = IndividualCall.State.ACCEPTED,
|
||||
direction = IndividualCall.Direction.OUTGOING
|
||||
)
|
||||
)
|
||||
}
|
||||
MessageTypes.isGroupCall(this.type) -> {
|
||||
try {
|
||||
val groupCallUpdateDetails = GroupCallUpdateDetailsUtil.parse(this.body)
|
||||
ChatUpdateMessage(
|
||||
groupCall = GroupCall(
|
||||
state = GroupCall.State.GENERIC,
|
||||
startedCallRecipientId = recipients.getByAci(ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid))).getOrNull()?.toLong(),
|
||||
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
|
||||
endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp
|
||||
)
|
||||
)
|
||||
} catch (exception: IOException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CallTable.Call.toCallUpdate(): ChatUpdateMessage? {
|
||||
return if (this.type == CallTable.Type.GROUP_CALL) {
|
||||
ChatUpdateMessage(
|
||||
groupCall = GroupCall(
|
||||
callId = this.messageId,
|
||||
state = when (this.event) {
|
||||
CallTable.Event.MISSED -> GroupCall.State.MISSED
|
||||
CallTable.Event.ONGOING -> GroupCall.State.GENERIC
|
||||
CallTable.Event.ACCEPTED -> GroupCall.State.ACCEPTED
|
||||
CallTable.Event.NOT_ACCEPTED -> GroupCall.State.GENERIC
|
||||
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> GroupCall.State.MISSED_NOTIFICATION_PROFILE
|
||||
CallTable.Event.GENERIC_GROUP_CALL -> GroupCall.State.GENERIC
|
||||
CallTable.Event.JOINED -> GroupCall.State.JOINED
|
||||
CallTable.Event.RINGING -> GroupCall.State.RINGING
|
||||
CallTable.Event.DECLINED -> GroupCall.State.DECLINED
|
||||
CallTable.Event.OUTGOING_RING -> GroupCall.State.OUTGOING_RING
|
||||
CallTable.Event.DELETE -> return null
|
||||
},
|
||||
ringerRecipientId = this.ringerRecipient?.toLong(),
|
||||
startedCallRecipientId = this.ringerRecipient?.toLong(),
|
||||
startedCallTimestamp = this.timestamp
|
||||
)
|
||||
)
|
||||
} else if (this.type != CallTable.Type.AD_HOC_CALL) {
|
||||
ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
callId = this.callId,
|
||||
type = if (this.type == CallTable.Type.VIDEO_CALL) IndividualCall.Type.VIDEO_CALL else IndividualCall.Type.AUDIO_CALL,
|
||||
direction = if (this.direction == CallTable.Direction.INCOMING) IndividualCall.Direction.INCOMING else IndividualCall.Direction.OUTGOING,
|
||||
state = when (this.event) {
|
||||
CallTable.Event.MISSED -> IndividualCall.State.MISSED
|
||||
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> IndividualCall.State.MISSED_NOTIFICATION_PROFILE
|
||||
CallTable.Event.ACCEPTED -> IndividualCall.State.ACCEPTED
|
||||
CallTable.Event.NOT_ACCEPTED -> IndividualCall.State.NOT_ACCEPTED
|
||||
else -> IndividualCall.State.UNKNOWN_STATE
|
||||
},
|
||||
startedCallTimestamp = this.timestamp
|
||||
)
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toPaymentNotificationUpdate(): PaymentNotification {
|
||||
val paymentUuid = UuidUtil.parseOrNull(this.body)
|
||||
val payment = if (paymentUuid != null) {
|
||||
SignalDatabase.payments.getPayment(paymentUuid)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return if (payment == null) {
|
||||
PaymentNotification()
|
||||
} else {
|
||||
PaymentNotification(
|
||||
amountMob = payment.amount.serializeAmountString(),
|
||||
feeMob = payment.fee.serializeAmountString(),
|
||||
note = payment.note,
|
||||
transactionDetails = payment.getTransactionDetails()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toStandardMessage(reactionRecords: List<ReactionRecord>?, mentions: List<Mention>?, attachments: List<DatabaseAttachment>?): StandardMessage {
|
||||
val text = if (body == null) {
|
||||
null
|
||||
@@ -390,7 +500,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
quote = this.toQuote(quotedAttachments),
|
||||
text = text,
|
||||
attachments = messageAttachments.toBackupAttachments(),
|
||||
// TODO Link previews!
|
||||
// TODO [backup] Link previews!
|
||||
linkPreview = emptyList(),
|
||||
longText = null,
|
||||
reactions = reactionRecords.toBackupReactions()
|
||||
@@ -416,6 +526,39 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupMessageRecord.toGiftBadgeUpdate(): BackupGiftBadge {
|
||||
val giftBadge = try {
|
||||
GiftBadge.ADAPTER.decode(Base64.decode(this.body ?: ""))
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to decode GiftBadge!")
|
||||
return BackupGiftBadge()
|
||||
}
|
||||
|
||||
return BackupGiftBadge(
|
||||
receiptCredentialPresentation = giftBadge.redemptionToken,
|
||||
state = when (giftBadge.redemptionState) {
|
||||
GiftBadge.RedemptionState.REDEEMED -> BackupGiftBadge.State.REDEEMED
|
||||
GiftBadge.RedemptionState.FAILED -> BackupGiftBadge.State.FAILED
|
||||
GiftBadge.RedemptionState.PENDING -> BackupGiftBadge.State.UNOPENED
|
||||
GiftBadge.RedemptionState.STARTED -> BackupGiftBadge.State.OPENED
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun DatabaseAttachment.toStickerMessage(reactions: List<ReactionRecord>?): StickerMessage {
|
||||
val stickerLocator = this.stickerLocator!!
|
||||
return StickerMessage(
|
||||
sticker = Sticker(
|
||||
packId = Hex.fromStringCondensed(stickerLocator.packId).toByteString(),
|
||||
packKey = Hex.fromStringCondensed(stickerLocator.packKey).toByteString(),
|
||||
stickerId = stickerLocator.stickerId,
|
||||
emoji = stickerLocator.emoji,
|
||||
data_ = this.toBackupAttachment().pointer
|
||||
),
|
||||
reactions = reactions.toBackupReactions()
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<DatabaseAttachment>.toBackupQuoteAttachments(): List<Quote.QuotedAttachment> {
|
||||
return this.map { attachment ->
|
||||
Quote.QuotedAttachment(
|
||||
@@ -444,7 +587,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
builder.backupLocator = FilePointer.BackupLocator(
|
||||
mediaName = archiveMediaName ?: this.getMediaName().toString(),
|
||||
cdnNumber = if (archiveMediaName != null) archiveCdn else Cdn.CDN_3.cdnNumber, // TODO (clark): Update when new proto with optional cdn is landed
|
||||
key = decode(remoteKey).toByteString(),
|
||||
key = Base64.decode(remoteKey).toByteString(),
|
||||
size = this.size.toInt(),
|
||||
digest = remoteDigest.toByteString()
|
||||
)
|
||||
@@ -456,7 +599,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
cdnKey = this.remoteLocation,
|
||||
cdnNumber = this.cdn.cdnNumber,
|
||||
uploadTimestamp = this.uploadTimestamp,
|
||||
key = decode(remoteKey).toByteString(),
|
||||
key = Base64.decode(remoteKey).toByteString(),
|
||||
size = this.size.toInt(),
|
||||
digest = remoteDigest.toByteString()
|
||||
)
|
||||
@@ -466,7 +609,16 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
return MessageAttachment(
|
||||
pointer = builder.build(),
|
||||
wasDownloaded = this.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || this.transferState == AttachmentTable.TRANSFER_NEEDS_RESTORE,
|
||||
flag = if (voiceNote) MessageAttachment.Flag.VOICE_MESSAGE else if (videoGif) MessageAttachment.Flag.GIF else if (borderless) MessageAttachment.Flag.BORDERLESS else MessageAttachment.Flag.NONE
|
||||
flag = if (voiceNote) {
|
||||
MessageAttachment.Flag.VOICE_MESSAGE
|
||||
} else if (videoGif) {
|
||||
MessageAttachment.Flag.GIF
|
||||
} else if (borderless) {
|
||||
MessageAttachment.Flag.BORDERLESS
|
||||
} else {
|
||||
MessageAttachment.Flag.NONE
|
||||
},
|
||||
clientUuid = uuid?.let { UuidUtil.toByteString(uuid) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -476,6 +628,46 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
}
|
||||
|
||||
private fun PaymentTable.PaymentTransaction.getTransactionDetails(): PaymentNotification.TransactionDetails? {
|
||||
if (failureReason != null || state == State.FAILED) {
|
||||
return PaymentNotification.TransactionDetails(failedTransaction = PaymentNotification.TransactionDetails.FailedTransaction(reason = failureReason.toBackupFailureReason()))
|
||||
}
|
||||
return PaymentNotification.TransactionDetails(
|
||||
transaction = PaymentNotification.TransactionDetails.Transaction(
|
||||
status = this.state.toBackupState(),
|
||||
timestamp = timestamp,
|
||||
blockIndex = blockIndex,
|
||||
blockTimestamp = blockTimestamp,
|
||||
mobileCoinIdentification = paymentMetaData.mobileCoinTxoIdentification?.toBackup()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun PaymentMetaData.MobileCoinTxoIdentification.toBackup(): PaymentNotification.TransactionDetails.MobileCoinTxoIdentification {
|
||||
return PaymentNotification.TransactionDetails.MobileCoinTxoIdentification(
|
||||
publicKey = this.publicKey,
|
||||
keyImages = this.keyImages
|
||||
)
|
||||
}
|
||||
|
||||
private fun State.toBackupState(): PaymentNotification.TransactionDetails.Transaction.Status {
|
||||
return when (this) {
|
||||
State.INITIAL -> PaymentNotification.TransactionDetails.Transaction.Status.INITIAL
|
||||
State.SUBMITTED -> PaymentNotification.TransactionDetails.Transaction.Status.SUBMITTED
|
||||
State.SUCCESSFUL -> PaymentNotification.TransactionDetails.Transaction.Status.SUCCESSFUL
|
||||
State.FAILED -> throw IllegalArgumentException("state cannot be failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun FailureReason?.toBackupFailureReason(): PaymentNotification.TransactionDetails.FailedTransaction.FailureReason {
|
||||
return when (this) {
|
||||
FailureReason.UNKNOWN -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.GENERIC
|
||||
FailureReason.INSUFFICIENT_FUNDS -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.INSUFFICIENT_FUNDS
|
||||
FailureReason.NETWORK -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.NETWORK
|
||||
else -> PaymentNotification.TransactionDetails.FailedTransaction.FailureReason.GENERIC
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Mention>.toBackupBodyRanges(): List<BackupBodyRange> {
|
||||
return this.map {
|
||||
BackupBodyRange(
|
||||
@@ -615,6 +807,28 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.isSmsType(): Boolean {
|
||||
if (MessageTypes.isSecureType(this)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (MessageTypes.isCallLog(this)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return MessageTypes.isOutgoingMessageType(this) || MessageTypes.isInboxType(this)
|
||||
}
|
||||
|
||||
private fun String.e164ToLong(): Long? {
|
||||
val fixed = if (this.startsWith("+")) {
|
||||
this.substring(1)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
return fixed.toLongOrNull()
|
||||
}
|
||||
|
||||
private fun Cursor.toBackupMessageRecord(): BackupMessageRecord {
|
||||
return BackupMessageRecord(
|
||||
id = this.requireLong(MessageTable.ID),
|
||||
@@ -637,8 +851,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
quoteMissing = this.requireBoolean(MessageTable.QUOTE_MISSING),
|
||||
quoteBodyRanges = this.requireBlob(MessageTable.QUOTE_BODY_RANGES),
|
||||
quoteType = this.requireInt(MessageTable.QUOTE_TYPE),
|
||||
originalMessageId = this.requireLong(MessageTable.ORIGINAL_MESSAGE_ID),
|
||||
latestRevisionId = this.requireLong(MessageTable.LATEST_REVISION_ID),
|
||||
originalMessageId = this.requireLongOrNull(MessageTable.ORIGINAL_MESSAGE_ID),
|
||||
latestRevisionId = this.requireLongOrNull(MessageTable.LATEST_REVISION_ID),
|
||||
hasDeliveryReceipt = this.requireBoolean(MessageTable.HAS_DELIVERY_RECEIPT),
|
||||
viewed = this.requireBoolean(MessageTable.VIEWED_COLUMN),
|
||||
hasReadReceipt = this.requireBoolean(MessageTable.HAS_READ_RECEIPT),
|
||||
@@ -672,8 +886,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
|
||||
val quoteMissing: Boolean,
|
||||
val quoteBodyRanges: ByteArray?,
|
||||
val quoteType: Int,
|
||||
val originalMessageId: Long,
|
||||
val latestRevisionId: Long,
|
||||
val originalMessageId: Long?,
|
||||
val latestRevisionId: Long?,
|
||||
val hasDeliveryReceipt: Boolean,
|
||||
val hasReadReceipt: Boolean,
|
||||
val viewed: Boolean,
|
||||
|
||||
@@ -7,7 +7,9 @@ package org.thoughtcrime.securesms.backup.v2.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.core.content.contentValuesOf
|
||||
import okio.ByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.orNull
|
||||
@@ -22,14 +24,17 @@ import org.thoughtcrime.securesms.backup.v2.BackupState
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Sticker
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||
@@ -38,6 +43,7 @@ import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.ReactionTable
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure
|
||||
@@ -45,22 +51,34 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
|
||||
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.CryptoValue
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PaymentTombstone
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.payments.CryptoValueUtil
|
||||
import org.thoughtcrime.securesms.payments.Direction
|
||||
import org.thoughtcrime.securesms.payments.State
|
||||
import org.thoughtcrime.securesms.payments.proto.PaymentMetaData
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.backup.MediaName
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
import org.whispersystems.signalservice.api.payments.Money
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import java.math.BigInteger
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
|
||||
|
||||
/**
|
||||
* An object that will ingest all fo the [ChatItem]s you want to write, buffer them until hitting a specified batch size, and then batch insert them
|
||||
@@ -92,7 +110,6 @@ class ChatItemImportInserter(
|
||||
MessageTable.EXPIRE_STARTED,
|
||||
MessageTable.UNIDENTIFIED,
|
||||
MessageTable.REMOTE_DELETED,
|
||||
MessageTable.REMOTE_DELETED,
|
||||
MessageTable.NETWORK_FAILURES,
|
||||
MessageTable.QUOTE_ID,
|
||||
MessageTable.QUOTE_AUTHOR,
|
||||
@@ -104,7 +121,9 @@ class ChatItemImportInserter(
|
||||
MessageTable.LINK_PREVIEWS,
|
||||
MessageTable.MESSAGE_RANGES,
|
||||
MessageTable.VIEW_ONCE,
|
||||
MessageTable.MESSAGE_EXTRAS
|
||||
MessageTable.MESSAGE_EXTRAS,
|
||||
MessageTable.ORIGINAL_MESSAGE_ID,
|
||||
MessageTable.LATEST_REVISION_ID
|
||||
)
|
||||
|
||||
private val REACTION_COLUMNS = arrayOf(
|
||||
@@ -156,8 +175,22 @@ class ChatItemImportInserter(
|
||||
Log.w(TAG, "[insert] Could not find a backup recipientId for backup chatId ${chatItem.chatId}! Skipping.")
|
||||
return
|
||||
}
|
||||
val messageInsert = chatItem.toMessageInsert(fromLocalRecipientId, chatLocalRecipientId, localThreadId)
|
||||
if (chatItem.revisions.isNotEmpty()) {
|
||||
val originalId = messageId
|
||||
val latestRevisionId = originalId + chatItem.revisions.size
|
||||
val sortedRevisions = chatItem.revisions.sortedBy { it.dateSent }.map { it.toMessageInsert(fromLocalRecipientId, chatLocalRecipientId, localThreadId) }
|
||||
for (revision in sortedRevisions) {
|
||||
revision.contentValues.put(MessageTable.ORIGINAL_MESSAGE_ID, originalId)
|
||||
revision.contentValues.put(MessageTable.LATEST_REVISION_ID, latestRevisionId)
|
||||
revision.contentValues.put(MessageTable.REVISION_NUMBER, (messageId - originalId))
|
||||
buffer.messages += revision
|
||||
messageId++
|
||||
}
|
||||
|
||||
buffer.messages += chatItem.toMessageInsert(fromLocalRecipientId, chatLocalRecipientId, localThreadId)
|
||||
messageInsert.contentValues.put(MessageTable.ORIGINAL_MESSAGE_ID, originalId)
|
||||
}
|
||||
buffer.messages += messageInsert
|
||||
buffer.reactions += chatItem.toReactionContentValues(messageId)
|
||||
buffer.groupReceipts += chatItem.toGroupReceiptContentValues(messageId, chatBackupRecipientId)
|
||||
|
||||
@@ -266,6 +299,22 @@ class ChatItemImportInserter(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.paymentNotification != null) {
|
||||
followUp = { messageRowId ->
|
||||
val uuid = tryRestorePayment(this, chatRecipientId)
|
||||
if (uuid != null) {
|
||||
db.update(
|
||||
MessageTable.TABLE_NAME,
|
||||
contentValuesOf(
|
||||
MessageTable.BODY to uuid.toString(),
|
||||
MessageTable.TYPE to ((contentValues.getAsLong(MessageTable.TYPE) and MessageTypes.SPECIAL_TYPES_MASK.inv()) or MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION)
|
||||
),
|
||||
"${MessageTable.ID}=?",
|
||||
SqlUtil.buildArgs(messageRowId)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.standardMessage != null) {
|
||||
val bodyRanges = this.standardMessage.text?.bodyRanges
|
||||
if (!bodyRanges.isNullOrEmpty()) {
|
||||
@@ -298,6 +347,15 @@ class ChatItemImportInserter(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.stickerMessage != null) {
|
||||
val sticker = this.stickerMessage.sticker
|
||||
val attachment = sticker.toLocalAttachment()
|
||||
if (attachment != null) {
|
||||
followUp = { messageRowId ->
|
||||
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(attachment), emptyList())
|
||||
}
|
||||
}
|
||||
}
|
||||
return MessageInsert(contentValues, followUp)
|
||||
}
|
||||
|
||||
@@ -353,11 +411,43 @@ class ChatItemImportInserter(
|
||||
this.standardMessage != null -> contentValues.addStandardMessage(this.standardMessage)
|
||||
this.remoteDeletedMessage != null -> contentValues.put(MessageTable.REMOTE_DELETED, 1)
|
||||
this.updateMessage != null -> contentValues.addUpdateMessage(this.updateMessage)
|
||||
this.paymentNotification != null -> contentValues.addPaymentNotification(this, chatRecipientId)
|
||||
this.giftBadge != null -> contentValues.addGiftBadge(this.giftBadge)
|
||||
}
|
||||
|
||||
return contentValues
|
||||
}
|
||||
|
||||
private fun tryRestorePayment(chatItem: ChatItem, chatRecipientId: RecipientId): UUID? {
|
||||
val paymentNotification = chatItem.paymentNotification!!
|
||||
|
||||
val amount = paymentNotification.amountMob?.tryParseMoney() ?: return null
|
||||
val fee = paymentNotification.feeMob?.tryParseMoney() ?: return null
|
||||
|
||||
if (paymentNotification.transactionDetails?.failedTransaction != null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val transaction = paymentNotification.transactionDetails?.transaction
|
||||
|
||||
val mobileCoinIdentification = transaction?.mobileCoinIdentification?.toLocal() ?: return null
|
||||
|
||||
return SignalDatabase.payments.restoreFromBackup(
|
||||
chatRecipientId,
|
||||
transaction.timestamp ?: 0,
|
||||
transaction.blockIndex ?: 0,
|
||||
paymentNotification.note ?: "",
|
||||
if (chatItem.outgoing != null) Direction.SENT else Direction.RECEIVED,
|
||||
transaction.status.toLocalStatus(),
|
||||
amount,
|
||||
fee,
|
||||
transaction.transaction?.toByteArray(),
|
||||
transaction.receipt?.toByteArray(),
|
||||
mobileCoinIdentification,
|
||||
chatItem.incoming?.read ?: true
|
||||
)
|
||||
}
|
||||
|
||||
private fun ChatItem.toReactionContentValues(messageId: Long): List<ContentValues> {
|
||||
val reactions: List<Reaction> = when {
|
||||
this.standardMessage != null -> this.standardMessage.reactions
|
||||
@@ -430,6 +520,10 @@ class ChatItemImportInserter(
|
||||
type = type or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT
|
||||
}
|
||||
|
||||
if (this.giftBadge != null) {
|
||||
type = type or MessageTypes.SPECIAL_TYPE_GIFT_BADGE
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
@@ -465,6 +559,7 @@ class ChatItemImportInserter(
|
||||
SimpleChatUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE or typeWithoutBase
|
||||
SimpleChatUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED or typeWithoutBase
|
||||
SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST or typeWithoutBase
|
||||
SimpleChatUpdate.Type.UNSUPPORTED_PROTOCOL_MESSAGE -> MessageTypes.UNSUPPORTED_MESSAGE_TYPE or typeWithoutBase
|
||||
}
|
||||
}
|
||||
updateMessage.expirationTimerChange != null -> {
|
||||
@@ -474,8 +569,14 @@ class ChatItemImportInserter(
|
||||
updateMessage.profileChange != null -> {
|
||||
typeFlags = MessageTypes.PROFILE_CHANGE_TYPE
|
||||
val profileChangeDetails = ProfileChangeDetails(profileNameChange = ProfileChangeDetails.StringChange(previous = updateMessage.profileChange.previousName, newValue = updateMessage.profileChange.newName))
|
||||
.encode()
|
||||
put(MessageTable.BODY, Base64.encodeWithPadding(profileChangeDetails))
|
||||
val messageExtras = MessageExtras(profileChangeDetails = profileChangeDetails).encode()
|
||||
put(MessageTable.MESSAGE_EXTRAS, messageExtras)
|
||||
}
|
||||
updateMessage.learnedProfileChange != null -> {
|
||||
typeFlags = MessageTypes.PROFILE_CHANGE_TYPE
|
||||
val profileChangeDetails = ProfileChangeDetails(learnedProfileName = ProfileChangeDetails.LearnedProfileName(e164 = updateMessage.learnedProfileChange.e164?.toString(), username = updateMessage.learnedProfileChange.username))
|
||||
val messageExtras = MessageExtras(profileChangeDetails = profileChangeDetails).encode()
|
||||
put(MessageTable.MESSAGE_EXTRAS, messageExtras)
|
||||
}
|
||||
updateMessage.sessionSwitchover != null -> {
|
||||
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
|
||||
@@ -500,7 +601,17 @@ class ChatItemImportInserter(
|
||||
this.put(MessageTable.TYPE, typeFlags)
|
||||
}
|
||||
updateMessage.groupCall != null -> {
|
||||
this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.groupCall))
|
||||
val startedCallRecipientId = if (updateMessage.groupCall.startedCallRecipientId != null) {
|
||||
backupState.backupToLocalRecipientId[updateMessage.groupCall.startedCallRecipientId]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val startedCall = if (startedCallRecipientId != null) {
|
||||
recipients.getRecord(startedCallRecipientId).aci
|
||||
} else {
|
||||
null
|
||||
}
|
||||
this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.groupCall, startedCall))
|
||||
this.put(MessageTable.TYPE, MessageTypes.GROUP_CALL_TYPE)
|
||||
}
|
||||
updateMessage.groupChange != null -> {
|
||||
@@ -518,6 +629,118 @@ class ChatItemImportInserter(
|
||||
this.put(MessageTable.TYPE, typeFlags)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the payment notification to the chat item.
|
||||
*
|
||||
* Note we add a tombstone first, then post insertion update it to a proper notification
|
||||
*/
|
||||
private fun ContentValues.addPaymentNotification(chatItem: ChatItem, chatRecipientId: RecipientId) {
|
||||
val paymentNotification = chatItem.paymentNotification!!
|
||||
if (chatItem.paymentNotification.amountMob.isNullOrEmpty()) {
|
||||
addPaymentTombstoneNoAmount()
|
||||
return
|
||||
}
|
||||
val amount = paymentNotification.amountMob?.tryParseMoney() ?: return addPaymentTombstoneNoAmount()
|
||||
val fee = paymentNotification.feeMob?.tryParseMoney() ?: return addPaymentTombstoneNoAmount()
|
||||
|
||||
if (chatItem.paymentNotification.transactionDetails?.failedTransaction != null) {
|
||||
addFailedPaymentNotification(chatItem, amount, fee, chatRecipientId)
|
||||
return
|
||||
}
|
||||
addPaymentTombstoneNoMetadata(chatItem.paymentNotification)
|
||||
}
|
||||
|
||||
private fun PaymentNotification.TransactionDetails.MobileCoinTxoIdentification.toLocal(): PaymentMetaData {
|
||||
return PaymentMetaData(
|
||||
mobileCoinTxoIdentification = PaymentMetaData.MobileCoinTxoIdentification(
|
||||
publicKey = this.publicKey,
|
||||
keyImages = this.keyImages
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun ContentValues.addFailedPaymentNotification(chatItem: ChatItem, amount: Money, fee: Money, chatRecipientId: RecipientId) {
|
||||
val uuid = SignalDatabase.payments.restoreFromBackup(
|
||||
chatRecipientId,
|
||||
0,
|
||||
0,
|
||||
chatItem.paymentNotification?.note ?: "",
|
||||
if (chatItem.outgoing != null) Direction.SENT else Direction.RECEIVED,
|
||||
State.FAILED,
|
||||
amount,
|
||||
fee,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
chatItem.incoming?.read ?: true
|
||||
)
|
||||
if (uuid != null) {
|
||||
put(MessageTable.BODY, uuid.toString())
|
||||
put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION)
|
||||
} else {
|
||||
addPaymentTombstoneNoMetadata(chatItem.paymentNotification!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun ContentValues.addPaymentTombstoneNoAmount() {
|
||||
put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or MessageTypes.SPECIAL_TYPE_PAYMENTS_TOMBSTONE)
|
||||
}
|
||||
|
||||
private fun ContentValues.addPaymentTombstoneNoMetadata(paymentNotification: PaymentNotification) {
|
||||
put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or MessageTypes.SPECIAL_TYPE_PAYMENTS_TOMBSTONE)
|
||||
val amount = tryParseCryptoValue(paymentNotification.amountMob)
|
||||
val fee = tryParseCryptoValue(paymentNotification.feeMob)
|
||||
put(
|
||||
MessageTable.MESSAGE_EXTRAS,
|
||||
MessageExtras(
|
||||
paymentTombstone = PaymentTombstone(
|
||||
note = paymentNotification.note,
|
||||
amount = amount,
|
||||
fee = fee
|
||||
)
|
||||
).encode()
|
||||
)
|
||||
}
|
||||
|
||||
private fun ContentValues.addGiftBadge(giftBadge: BackupGiftBadge) {
|
||||
val dbGiftBadge = GiftBadge(
|
||||
redemptionToken = giftBadge.receiptCredentialPresentation,
|
||||
redemptionState = when (giftBadge.state) {
|
||||
BackupGiftBadge.State.UNOPENED -> GiftBadge.RedemptionState.PENDING
|
||||
BackupGiftBadge.State.OPENED -> GiftBadge.RedemptionState.STARTED
|
||||
BackupGiftBadge.State.REDEEMED -> GiftBadge.RedemptionState.REDEEMED
|
||||
BackupGiftBadge.State.FAILED -> GiftBadge.RedemptionState.FAILED
|
||||
}
|
||||
)
|
||||
|
||||
put(MessageTable.BODY, Base64.encodeWithPadding(GiftBadge.ADAPTER.encode(dbGiftBadge)))
|
||||
}
|
||||
|
||||
private fun String?.tryParseMoney(): Money? {
|
||||
if (this.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val amountCryptoValue = tryParseCryptoValue(this)
|
||||
return if (amountCryptoValue != null) {
|
||||
CryptoValueUtil.cryptoValueToMoney(amountCryptoValue)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun tryParseCryptoValue(bigIntegerString: String?): CryptoValue? {
|
||||
if (bigIntegerString == null) {
|
||||
return null
|
||||
}
|
||||
val amount = try {
|
||||
BigInteger(bigIntegerString).toString()
|
||||
} catch (e: NumberFormatException) {
|
||||
return null
|
||||
}
|
||||
return CryptoValue(mobileCoinValue = CryptoValue.MobileCoinValue(picoMobileCoin = amount))
|
||||
}
|
||||
|
||||
private fun ContentValues.addQuote(quote: Quote) {
|
||||
this.put(MessageTable.QUOTE_ID, quote.targetSentTimestamp ?: MessageTable.QUOTE_TARGET_MISSING_ID)
|
||||
this.put(MessageTable.QUOTE_AUTHOR, backupState.backupToLocalRecipientId[quote.authorId]!!.serialize())
|
||||
@@ -528,6 +751,15 @@ class ChatItemImportInserter(
|
||||
this.put(MessageTable.QUOTE_MISSING, (quote.targetSentTimestamp == null).toInt())
|
||||
}
|
||||
|
||||
private fun PaymentNotification.TransactionDetails.Transaction.Status?.toLocalStatus(): State {
|
||||
return when (this) {
|
||||
PaymentNotification.TransactionDetails.Transaction.Status.INITIAL -> State.INITIAL
|
||||
PaymentNotification.TransactionDetails.Transaction.Status.SUBMITTED -> State.SUBMITTED
|
||||
PaymentNotification.TransactionDetails.Transaction.Status.SUCCESSFUL -> State.SUCCESSFUL
|
||||
else -> State.INITIAL
|
||||
}
|
||||
}
|
||||
|
||||
private fun Quote.Type.toLocalQuoteType(): Int {
|
||||
return when (this) {
|
||||
Quote.Type.UNKNOWN -> QuoteModel.Type.NORMAL.code
|
||||
@@ -607,80 +839,129 @@ class ChatItemImportInserter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageAttachment.toLocalAttachment(contentType: String? = pointer?.contentType, fileName: String? = pointer?.fileName): Attachment? {
|
||||
if (pointer == null) return null
|
||||
if (pointer.attachmentLocator != null) {
|
||||
private fun FilePointer?.toLocalAttachment(voiceNote: Boolean, borderless: Boolean, gif: Boolean, wasDownloaded: Boolean, stickerLocator: StickerLocator? = null, contentType: String? = this?.contentType, fileName: String? = this?.fileName, uuid: ByteString? = null): Attachment? {
|
||||
if (this == null) return null
|
||||
|
||||
if (attachmentLocator != null) {
|
||||
val signalAttachmentPointer = SignalServiceAttachmentPointer(
|
||||
pointer.attachmentLocator.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(pointer.attachmentLocator.cdnKey),
|
||||
attachmentLocator.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(attachmentLocator.cdnKey),
|
||||
contentType,
|
||||
pointer.attachmentLocator.key.toByteArray(),
|
||||
Optional.ofNullable(pointer.attachmentLocator.size),
|
||||
attachmentLocator.key.toByteArray(),
|
||||
Optional.ofNullable(attachmentLocator.size),
|
||||
Optional.empty(),
|
||||
pointer.width ?: 0,
|
||||
pointer.height ?: 0,
|
||||
Optional.ofNullable(pointer.attachmentLocator.digest.toByteArray()),
|
||||
Optional.ofNullable(pointer.incrementalMac?.toByteArray()),
|
||||
pointer.incrementalMacChunkSize ?: 0,
|
||||
width ?: 0,
|
||||
height ?: 0,
|
||||
Optional.ofNullable(attachmentLocator.digest.toByteArray()),
|
||||
Optional.ofNullable(incrementalMac?.toByteArray()),
|
||||
incrementalMacChunkSize ?: 0,
|
||||
Optional.ofNullable(fileName),
|
||||
flag == MessageAttachment.Flag.VOICE_MESSAGE,
|
||||
flag == MessageAttachment.Flag.BORDERLESS,
|
||||
flag == MessageAttachment.Flag.GIF,
|
||||
Optional.ofNullable(pointer.caption),
|
||||
Optional.ofNullable(pointer.blurHash),
|
||||
pointer.attachmentLocator.uploadTimestamp
|
||||
voiceNote,
|
||||
borderless,
|
||||
gif,
|
||||
Optional.ofNullable(caption),
|
||||
Optional.ofNullable(blurHash),
|
||||
attachmentLocator.uploadTimestamp,
|
||||
UuidUtil.fromByteStringOrNull(uuid)
|
||||
)
|
||||
return PointerAttachment.forPointer(
|
||||
pointer = Optional.of(signalAttachmentPointer),
|
||||
stickerLocator = stickerLocator,
|
||||
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING
|
||||
).orNull()
|
||||
} else if (pointer.invalidAttachmentLocator != null) {
|
||||
} else if (invalidAttachmentLocator != null) {
|
||||
return TombstoneAttachment(
|
||||
contentType = contentType,
|
||||
incrementalMac = pointer.incrementalMac?.toByteArray(),
|
||||
incrementalMacChunkSize = pointer.incrementalMacChunkSize,
|
||||
width = pointer.width,
|
||||
height = pointer.height,
|
||||
caption = pointer.caption,
|
||||
blurHash = pointer.blurHash,
|
||||
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
|
||||
borderless = flag == MessageAttachment.Flag.BORDERLESS,
|
||||
gif = flag == MessageAttachment.Flag.GIF,
|
||||
quote = false
|
||||
incrementalMac = incrementalMac?.toByteArray(),
|
||||
incrementalMacChunkSize = incrementalMacChunkSize,
|
||||
width = width,
|
||||
height = height,
|
||||
caption = caption,
|
||||
blurHash = blurHash,
|
||||
voiceNote = voiceNote,
|
||||
borderless = borderless,
|
||||
gif = gif,
|
||||
quote = false,
|
||||
uuid = UuidUtil.fromByteStringOrNull(uuid)
|
||||
)
|
||||
} else if (pointer.backupLocator != null) {
|
||||
} else if (backupLocator != null) {
|
||||
return ArchivedAttachment(
|
||||
contentType = contentType,
|
||||
size = pointer.backupLocator.size.toLong(),
|
||||
cdn = pointer.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
|
||||
key = pointer.backupLocator.key.toByteArray(),
|
||||
cdnKey = pointer.backupLocator.transitCdnKey,
|
||||
archiveCdn = pointer.backupLocator.cdnNumber,
|
||||
archiveMediaName = pointer.backupLocator.mediaName,
|
||||
archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(pointer.backupLocator.mediaName)).encode(),
|
||||
archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(pointer.backupLocator.mediaName)).encode(),
|
||||
digest = pointer.backupLocator.digest.toByteArray(),
|
||||
incrementalMac = pointer.incrementalMac?.toByteArray(),
|
||||
incrementalMacChunkSize = pointer.incrementalMacChunkSize,
|
||||
width = pointer.width,
|
||||
height = pointer.height,
|
||||
caption = pointer.caption,
|
||||
blurHash = pointer.blurHash,
|
||||
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
|
||||
borderless = flag == MessageAttachment.Flag.BORDERLESS,
|
||||
gif = flag == MessageAttachment.Flag.GIF,
|
||||
quote = false
|
||||
size = backupLocator.size.toLong(),
|
||||
cdn = backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
|
||||
key = backupLocator.key.toByteArray(),
|
||||
cdnKey = backupLocator.transitCdnKey,
|
||||
archiveCdn = backupLocator.cdnNumber,
|
||||
archiveMediaName = backupLocator.mediaName,
|
||||
archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(backupLocator.mediaName)).encode(),
|
||||
archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(backupLocator.mediaName)).encode(),
|
||||
digest = backupLocator.digest.toByteArray(),
|
||||
incrementalMac = incrementalMac?.toByteArray(),
|
||||
incrementalMacChunkSize = incrementalMacChunkSize,
|
||||
width = width,
|
||||
height = height,
|
||||
caption = caption,
|
||||
blurHash = blurHash,
|
||||
voiceNote = voiceNote,
|
||||
borderless = borderless,
|
||||
gif = gif,
|
||||
quote = false,
|
||||
stickerLocator = stickerLocator,
|
||||
uuid = UuidUtil.fromByteStringOrNull(uuid)
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun Sticker?.toLocalAttachment(): Attachment? {
|
||||
if (this == null) return null
|
||||
|
||||
return data_.toLocalAttachment(
|
||||
voiceNote = false,
|
||||
gif = false,
|
||||
borderless = false,
|
||||
wasDownloaded = true,
|
||||
stickerLocator = StickerLocator(
|
||||
packId = Hex.toStringCondensed(packId.toByteArray()),
|
||||
packKey = Hex.toStringCondensed(packKey.toByteArray()),
|
||||
stickerId = stickerId,
|
||||
emoji = emoji
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun MessageAttachment.toLocalAttachment(): Attachment? {
|
||||
return pointer?.toLocalAttachment(
|
||||
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
|
||||
gif = flag == MessageAttachment.Flag.GIF,
|
||||
borderless = flag == MessageAttachment.Flag.BORDERLESS,
|
||||
wasDownloaded = wasDownloaded,
|
||||
uuid = clientUuid
|
||||
)
|
||||
}
|
||||
|
||||
private fun MessageAttachment.toLocalAttachment(contentType: String?, fileName: String?): Attachment? {
|
||||
return pointer?.toLocalAttachment(
|
||||
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
|
||||
gif = flag == MessageAttachment.Flag.GIF,
|
||||
borderless = flag == MessageAttachment.Flag.BORDERLESS,
|
||||
wasDownloaded = wasDownloaded,
|
||||
contentType = contentType,
|
||||
fileName = fileName,
|
||||
uuid = clientUuid
|
||||
)
|
||||
}
|
||||
|
||||
private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? {
|
||||
return thumbnail?.toLocalAttachment(this.contentType, this.fileName)
|
||||
?: if (this.contentType == null) null else PointerAttachment.forPointer(quotedAttachment = DataMessage.Quote.QuotedAttachment(contentType = this.contentType, fileName = this.fileName, thumbnail = null)).orNull()
|
||||
}
|
||||
|
||||
private class MessageInsert(val contentValues: ContentValues, val followUp: ((Long) -> Unit)?)
|
||||
private class MessageInsert(
|
||||
val contentValues: ContentValues,
|
||||
val followUp: ((Long) -> Unit)?,
|
||||
val edits: List<MessageInsert>? = null
|
||||
)
|
||||
|
||||
private class Buffer(
|
||||
val messages: MutableList<MessageInsert> = mutableListOf(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user