mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-18 16:57:46 +00:00
Compare commits
494 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
eb114de5c8 | ||
|
|
1bf9695cff | ||
|
|
241bf065e8 | ||
|
|
e0f3b35805 | ||
|
|
5741dfc00b | ||
|
|
ec430da772 | ||
|
|
5e6d9434de | ||
|
|
b72d586748 | ||
|
|
757c0fd2ea | ||
|
|
c4e4eaf110 | ||
|
|
f83275e246 | ||
|
|
d0340d39db | ||
|
|
227a279131 | ||
|
|
0465fdea62 | ||
|
|
13bd4a9c74 | ||
|
|
f570f1f2c4 | ||
|
|
68ced18ea1 | ||
|
|
b4a8f01980 | ||
|
|
c3c743fbb8 | ||
|
|
b14eddefc9 | ||
|
|
46638a1948 | ||
|
|
5cee85fcdc | ||
|
|
f97d7e3dfd | ||
|
|
6da0ecf827 | ||
|
|
9803550bba | ||
|
|
15284da4c5 | ||
|
|
351c3219e4 | ||
|
|
ab95dbbc77 | ||
|
|
cc6cba45c6 | ||
|
|
ce37660df2 | ||
|
|
ca14ed9b2c | ||
|
|
ba4cdea75d | ||
|
|
83c34dd4cc | ||
|
|
b6db3802d3 | ||
|
|
a9a19d3ae0 | ||
|
|
52fb873b1b | ||
|
|
9a0bb243cd | ||
|
|
78bbab37fb | ||
|
|
9af73b1409 | ||
|
|
9c5bb4aa17 | ||
|
|
49ba83dda8 | ||
|
|
de3b0d4ca2 | ||
|
|
b2efc42357 | ||
|
|
a71faf674d | ||
|
|
34faa9003f | ||
|
|
bc527a2bc1 | ||
|
|
0a3f96935a | ||
|
|
35232a3928 | ||
|
|
70d74e0bb1 | ||
|
|
36c91a95e2 | ||
|
|
4600e38a2a | ||
|
|
55abd88a03 | ||
|
|
cd880b0879 | ||
|
|
bbae6d876f | ||
|
|
48a0c5a5a9 | ||
|
|
c261df41b0 | ||
|
|
cc98eced27 | ||
|
|
452d5960e4 | ||
|
|
c95b180728 | ||
|
|
3c380d35fd | ||
|
|
41935120e5 | ||
|
|
03d8f72c41 | ||
|
|
ab9ecff4d4 | ||
|
|
e351a0b235 | ||
|
|
4a08de370a | ||
|
|
6d657b449c | ||
|
|
adef572abb | ||
|
|
d6f2039bd1 | ||
|
|
1223c3c768 | ||
|
|
333fa22c96 | ||
|
|
76c04d8d6d | ||
|
|
c3070f2913 | ||
|
|
234b3967ed | ||
|
|
89d420cda8 | ||
|
|
ced4ece5b8 | ||
|
|
8c81e47737 | ||
|
|
5d15eef61d | ||
|
|
8f3e62245f | ||
|
|
e4ab795c62 | ||
|
|
e4d6f9240f | ||
|
|
cfaf40e605 | ||
|
|
bdcf2431e7 | ||
|
|
7241283be2 | ||
|
|
dde2a8b63a | ||
|
|
f7763a5b82 | ||
|
|
c6f4a01001 | ||
|
|
95a6835988 | ||
|
|
f9a8f447d2 | ||
|
|
d20f588802 | ||
|
|
f23476a4e9 | ||
|
|
fd4864b3b1 | ||
|
|
c5c0c432c4 | ||
|
|
69c40a6835 | ||
|
|
7ef7aa65e6 | ||
|
|
97c08f0d52 | ||
|
|
18e6c57e75 | ||
|
|
ffc1463cda | ||
|
|
84e654efb2 | ||
|
|
d983265e08 | ||
|
|
e60b32202e | ||
|
|
95fbd7a31c | ||
|
|
00a91e32fc | ||
|
|
fa32b7a883 | ||
|
|
63e6f955ed | ||
|
|
7dcb8a425a | ||
|
|
f35ce068f9 | ||
|
|
881d231a93 | ||
|
|
293634c758 | ||
|
|
4134df3f35 | ||
|
|
f78a019c70 | ||
|
|
d561a1385c | ||
|
|
9b5387e221 | ||
|
|
25b1a814fe | ||
|
|
b043b6e458 | ||
|
|
8a972d93e9 | ||
|
|
8fe66a14c5 | ||
|
|
f82bd64c10 | ||
|
|
4bcab49539 | ||
|
|
0f4618ab11 | ||
|
|
475ca50fab | ||
|
|
a64a02fa0c | ||
|
|
f3669a5865 | ||
|
|
34dbd11db0 | ||
|
|
2e7279c72f | ||
|
|
6ad72f00af | ||
|
|
b771a21518 | ||
|
|
04fb459acd | ||
|
|
690a68f0d0 | ||
|
|
f34ae8d118 | ||
|
|
da43ff1e95 | ||
|
|
f053ebbd51 | ||
|
|
87606af29c | ||
|
|
c811bdcffa | ||
|
|
0536628da3 | ||
|
|
1fa53cfcb8 | ||
|
|
a9ea3854d2 | ||
|
|
dc35261e00 | ||
|
|
716bc1f5e7 | ||
|
|
db27204084 | ||
|
|
42aeceffe2 | ||
|
|
03845eabaf | ||
|
|
62af9dad50 | ||
|
|
ee58d47926 | ||
|
|
d74260b536 | ||
|
|
15d8a698c5 | ||
|
|
62cf3feeaa | ||
|
|
947ab7d48b | ||
|
|
a82b9ee25f | ||
|
|
1e4d96b7c4 | ||
|
|
735a8e680c | ||
|
|
d9e9fe1d6a | ||
|
|
4bcd1df4f8 | ||
|
|
9762899272 | ||
|
|
ce1b73970c | ||
|
|
58282e589b | ||
|
|
75bd113545 | ||
|
|
7a6bd0e1f2 | ||
|
|
f673c4eb83 | ||
|
|
cbb04e8f0c | ||
|
|
cd03da54d5 | ||
|
|
5f31f5966c | ||
|
|
d8bbfe2678 | ||
|
|
7a2d408ca2 | ||
|
|
5e4dfcc65f | ||
|
|
7811e51b41 | ||
|
|
9703a868e5 | ||
|
|
1b7784b01f | ||
|
|
a83abaca1d | ||
|
|
29b3f09d8a | ||
|
|
d36b2a23f5 | ||
|
|
8f1722c718 | ||
|
|
5416c3b8aa | ||
|
|
89eeae36c4 | ||
|
|
eec2685e67 | ||
|
|
318b59a6b2 | ||
|
|
a2e0468cd9 | ||
|
|
689eacd618 | ||
|
|
8617a074ad | ||
|
|
046b8da880 | ||
|
|
34a36ddfea | ||
|
|
9330448198 | ||
|
|
b3336b4d84 | ||
|
|
9553c94097 | ||
|
|
c1845ae1c4 | ||
|
|
b6cc3852b0 | ||
|
|
eefc86f27e | ||
|
|
09404157aa | ||
|
|
abfd9f8f41 | ||
|
|
e04381fd75 | ||
|
|
30cc3ff9fc | ||
|
|
6f5f299035 | ||
|
|
02eed02cb8 | ||
|
|
c1d29b5c39 | ||
|
|
db4442939d | ||
|
|
6ece776382 | ||
|
|
0eda714755 | ||
|
|
831d099503 | ||
|
|
fa23e4ca70 | ||
|
|
982f602178 | ||
|
|
713298109a | ||
|
|
8793981804 | ||
|
|
9bd4e9524c | ||
|
|
791dc2724f | ||
|
|
ba3473c61a | ||
|
|
3ea194255d | ||
|
|
ea081e981f | ||
|
|
2ce6ea9a2a | ||
|
|
295c9310e9 | ||
|
|
7447ed2eac | ||
|
|
d5bf16b91a | ||
|
|
76665c1f0d | ||
|
|
dd28523b05 | ||
|
|
16588c401e | ||
|
|
dbf8a7ca87 | ||
|
|
e92c76434e | ||
|
|
7adb581271 | ||
|
|
869476a41b | ||
|
|
8daf1bca20 | ||
|
|
d044b3c931 | ||
|
|
0fcb19e1cc | ||
|
|
2a6977da75 | ||
|
|
26bd435bf6 | ||
|
|
91f8d6075c | ||
|
|
9ed9a330f4 | ||
|
|
8bbf6b790f | ||
|
|
a277e9b307 | ||
|
|
f8e6bcf290 | ||
|
|
3ba2b46bb0 | ||
|
|
b50eab230d | ||
|
|
3f91824325 | ||
|
|
879e05148b | ||
|
|
78e36b85d4 | ||
|
|
544cc06f13 | ||
|
|
133b7ef3f1 | ||
|
|
08a407dc23 | ||
|
|
6c697fad8b | ||
|
|
c904a7aa97 | ||
|
|
ad131d7c65 | ||
|
|
e12d2d1e98 | ||
|
|
f01e044662 | ||
|
|
03d3ae7043 | ||
|
|
6b60a22879 | ||
|
|
bbded8caa8 | ||
|
|
3a6352d2a3 | ||
|
|
8293d6bc4c | ||
|
|
56bdb28c2f | ||
|
|
b081fb1e13 | ||
|
|
58c1f64dfe | ||
|
|
92b7147dcd | ||
|
|
fa3a85c948 | ||
|
|
9da4513694 | ||
|
|
de520036a9 | ||
|
|
97ca15a1c0 | ||
|
|
713a34a5e7 | ||
|
|
d688280a30 |
@@ -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
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,7 +2,7 @@
|
||||
### First time contributor checklist
|
||||
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
|
||||
- [ ] I have read [how to contribute](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md) to this project
|
||||
- [ ] I have signed the [Contributor License Agreement](https://whispersystems.org/cla/)
|
||||
- [ ] I have signed the [Contributor License Agreement](https://signal.org/cla/)
|
||||
|
||||
### Contributor checklist
|
||||
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "libwebp"]
|
||||
path = libwebp
|
||||
url = https://github.com/webmproject/libwebp.git
|
||||
@@ -21,17 +21,10 @@ plugins {
|
||||
|
||||
apply(from = "static-ips.gradle.kts")
|
||||
|
||||
val canonicalVersionCode = 1403
|
||||
val canonicalVersionName = "7.2.2"
|
||||
|
||||
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 = 1431
|
||||
val canonicalVersionName = "7.10.2"
|
||||
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
|
||||
@@ -178,7 +173,6 @@ android {
|
||||
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\"")
|
||||
@@ -209,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\"")
|
||||
@@ -377,7 +372,6 @@ android {
|
||||
buildConfigField("String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3-staging.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
|
||||
@@ -388,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\"")
|
||||
@@ -407,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")) {
|
||||
@@ -421,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,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")
|
||||
@@ -542,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) {
|
||||
@@ -553,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)
|
||||
@@ -714,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,9 +5,7 @@
|
||||
|
||||
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 okio.ByteString.Companion.toByteString
|
||||
import org.junit.Assert
|
||||
@@ -16,22 +14,26 @@ 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
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Call
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Chat
|
||||
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
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Quote
|
||||
@@ -49,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
|
||||
@@ -77,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",
|
||||
@@ -85,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,
|
||||
@@ -109,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",
|
||||
@@ -128,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
|
||||
@@ -156,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(
|
||||
@@ -168,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",
|
||||
@@ -194,8 +210,7 @@ class ImportExportTest {
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = true,
|
||||
hideStory = false,
|
||||
storySendMode = Group.StorySendMode.ENABLED,
|
||||
name = "Cool Group $i"
|
||||
storySendMode = Group.StorySendMode.ENABLED
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -215,7 +230,7 @@ class ImportExportTest {
|
||||
|
||||
@Test
|
||||
fun largeNumberOfMessagesAndChats() {
|
||||
val NUM_INDIVIDUAL_RECIPIENTS = 1000
|
||||
val numIndividualRecipients = 1000
|
||||
val numIndividualMessages = 500
|
||||
val numGroupMessagesPerPerson = 200
|
||||
|
||||
@@ -224,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(
|
||||
@@ -235,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",
|
||||
@@ -261,8 +275,7 @@ class ImportExportTest {
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = random.trueWithProbability(0.9f),
|
||||
hideStory = random.trueWithProbability(0.1f),
|
||||
storySendMode = if (random.trueWithProbability(0.9f)) Group.StorySendMode.ENABLED else Group.StorySendMode.DISABLED,
|
||||
name = "Cool Group $i"
|
||||
storySendMode = if (random.trueWithProbability(0.9f)) Group.StorySendMode.ENABLED else Group.StorySendMode.DISABLED
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -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,23 +436,33 @@ class ImportExportTest {
|
||||
importExport(
|
||||
*standardFrames,
|
||||
Recipient(
|
||||
id = 3,
|
||||
id = 4,
|
||||
group = Group(
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = true,
|
||||
hideStory = true,
|
||||
storySendMode = Group.StorySendMode.ENABLED,
|
||||
name = "Cool test group"
|
||||
snapshot = Group.GroupSnapshot(
|
||||
title = Group.GroupAttributeBlob(title = "Group Cool"),
|
||||
description = Group.GroupAttributeBlob(descriptionText = "Description"),
|
||||
version = 10,
|
||||
disappearingMessagesTimer = Group.GroupAttributeBlob(disappearingMessagesDuration = 1500000)
|
||||
)
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 4,
|
||||
id = 5,
|
||||
group = Group(
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = false,
|
||||
hideStory = false,
|
||||
storySendMode = Group.StorySendMode.DEFAULT,
|
||||
name = "Cool test group"
|
||||
snapshot = Group.GroupSnapshot(
|
||||
title = Group.GroupAttributeBlob(title = "Group Cool"),
|
||||
description = Group.GroupAttributeBlob(descriptionText = "Description"),
|
||||
version = 10,
|
||||
disappearingMessagesTimer = Group.GroupAttributeBlob(disappearingMessagesDuration = 1500000)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -452,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",
|
||||
@@ -469,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",
|
||||
@@ -506,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)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -522,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",
|
||||
@@ -544,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)
|
||||
}
|
||||
|
||||
@@ -569,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",
|
||||
@@ -587,100 +600,98 @@ class ImportExportTest {
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 4,
|
||||
id = 5,
|
||||
group = Group(
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = true,
|
||||
hideStory = true,
|
||||
storySendMode = Group.StorySendMode.DEFAULT,
|
||||
name = "Cool test group"
|
||||
storySendMode = Group.StorySendMode.DEFAULT
|
||||
)
|
||||
),
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun calls() {
|
||||
val individualCalls = ArrayList<Call>()
|
||||
val groupCalls = ArrayList<Call>()
|
||||
val states = arrayOf(Call.State.MISSED, Call.State.COMPLETED, Call.State.DECLINED_BY_USER, Call.State.DECLINED_BY_NOTIFICATION_PROFILE)
|
||||
val types = arrayOf(Call.Type.VIDEO_CALL, Call.Type.AD_HOC_CALL, Call.Type.AUDIO_CALL)
|
||||
var id = 1L
|
||||
var timestamp = 12345L
|
||||
|
||||
fun individualCalls() {
|
||||
val individualCalls = ArrayList<ChatItem>()
|
||||
val states = arrayOf(IndividualCall.State.ACCEPTED, IndividualCall.State.NOT_ACCEPTED, IndividualCall.State.MISSED, IndividualCall.State.MISSED_NOTIFICATION_PROFILE)
|
||||
val oldStates = arrayOf(IndividualCall.State.ACCEPTED, IndividualCall.State.MISSED)
|
||||
val types = arrayOf(IndividualCall.Type.VIDEO_CALL, IndividualCall.Type.AUDIO_CALL)
|
||||
val directions = arrayOf(IndividualCall.Direction.OUTGOING, IndividualCall.Direction.INCOMING)
|
||||
var sentTime = 0L
|
||||
var callId = 1L
|
||||
val startedAci = TestRecipientUtils.nextAci().toByteString()
|
||||
for (state in states) {
|
||||
for (type in types) {
|
||||
individualCalls.add(
|
||||
Call(
|
||||
callId = id++,
|
||||
conversationRecipientId = 3,
|
||||
type = type,
|
||||
state = state,
|
||||
timestamp = timestamp++,
|
||||
ringerRecipientId = 3,
|
||||
outgoing = true
|
||||
for (direction in directions) {
|
||||
// With call id
|
||||
individualCalls.add(
|
||||
ChatItem(
|
||||
chatId = 1,
|
||||
authorId = selfRecipient.id,
|
||||
dateSent = sentTime++,
|
||||
sms = false,
|
||||
directionless = ChatItem.DirectionlessMessageDetails(),
|
||||
updateMessage = ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
callId = callId++,
|
||||
type = type,
|
||||
state = state,
|
||||
direction = direction
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
individualCalls.add(
|
||||
Call(
|
||||
callId = id++,
|
||||
conversationRecipientId = 3,
|
||||
type = type,
|
||||
state = state,
|
||||
timestamp = timestamp++,
|
||||
ringerRecipientId = selfRecipient.id,
|
||||
outgoing = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (state in oldStates) {
|
||||
for (type in types) {
|
||||
for (direction in directions) {
|
||||
if (state == IndividualCall.State.MISSED && direction == IndividualCall.Direction.OUTGOING) continue
|
||||
// Without call id
|
||||
individualCalls.add(
|
||||
ChatItem(
|
||||
chatId = 1,
|
||||
authorId = selfRecipient.id,
|
||||
dateSent = sentTime++,
|
||||
sms = false,
|
||||
directionless = ChatItem.DirectionlessMessageDetails(),
|
||||
updateMessage = ChatUpdateMessage(
|
||||
individualCall = IndividualCall(
|
||||
callId = null,
|
||||
type = type,
|
||||
state = state,
|
||||
direction = direction
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
groupCalls.add(
|
||||
Call(
|
||||
callId = id++,
|
||||
conversationRecipientId = 4,
|
||||
type = Call.Type.GROUP_CALL,
|
||||
state = state,
|
||||
timestamp = timestamp++,
|
||||
ringerRecipientId = 3,
|
||||
outgoing = true
|
||||
)
|
||||
)
|
||||
groupCalls.add(
|
||||
Call(
|
||||
callId = id++,
|
||||
conversationRecipientId = 4,
|
||||
type = Call.Type.GROUP_CALL,
|
||||
state = state,
|
||||
timestamp = timestamp++,
|
||||
ringerRecipientId = selfRecipient.id,
|
||||
outgoing = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
importExport(
|
||||
*standardFrames,
|
||||
Recipient(
|
||||
id = 3,
|
||||
id = 4,
|
||||
contact = Contact(
|
||||
aci = TestRecipientUtils.nextAci().toByteString(),
|
||||
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",
|
||||
@@ -689,17 +700,25 @@ class ImportExportTest {
|
||||
)
|
||||
),
|
||||
Recipient(
|
||||
id = 4,
|
||||
id = 5,
|
||||
group = Group(
|
||||
masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(),
|
||||
whitelisted = true,
|
||||
hideStory = true,
|
||||
storySendMode = Group.StorySendMode.DEFAULT,
|
||||
name = "Cool test group"
|
||||
storySendMode = Group.StorySendMode.DEFAULT
|
||||
)
|
||||
),
|
||||
*individualCalls.toArray(),
|
||||
*groupCalls.toArray()
|
||||
Chat(
|
||||
id = 1,
|
||||
recipientId = 4,
|
||||
archived = true,
|
||||
pinnedOrder = 1,
|
||||
expirationTimerMs = 1.days.inWholeMilliseconds,
|
||||
muteUntilMs = System.currentTimeMillis(),
|
||||
markedUnread = true,
|
||||
dontNotifyForMentionsIfMuted = true
|
||||
),
|
||||
*individualCalls.toArray()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -936,7 +955,7 @@ class ImportExportTest {
|
||||
chatId = 1,
|
||||
authorId = alice.id,
|
||||
dateSent = 101,
|
||||
expireStartDate = null,
|
||||
expireStartDate = 0,
|
||||
expiresInMs = TimeUnit.DAYS.toMillis(1),
|
||||
sms = false,
|
||||
incoming = ChatItem.IncomingMessageDetails(
|
||||
@@ -975,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)
|
||||
}
|
||||
|
||||
@@ -1008,17 +1026,30 @@ class ImportExportTest {
|
||||
attachmentLocator = FilePointer.AttachmentLocator(
|
||||
cdnKey = "coolCdnKey",
|
||||
cdnNumber = 2,
|
||||
uploadTimestamp = System.currentTimeMillis()
|
||||
uploadTimestamp = System.currentTimeMillis(),
|
||||
key = (1..32).map { it.toByte() }.toByteArray().toByteString(),
|
||||
size = 12345,
|
||||
digest = (1..32).map { it.toByte() }.toByteArray().toByteString()
|
||||
),
|
||||
key = (1..32).map { it.toByte() }.toByteArray().toByteString(),
|
||||
contentType = "image/png",
|
||||
size = 12345,
|
||||
fileName = "very_cool_picture.png",
|
||||
width = 100,
|
||||
height = 200,
|
||||
caption = "Love this cool picture!",
|
||||
incrementalMacChunkSize = 0
|
||||
)
|
||||
),
|
||||
wasDownloaded = true
|
||||
),
|
||||
MessageAttachment(
|
||||
pointer = FilePointer(
|
||||
invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator(),
|
||||
contentType = "image/png",
|
||||
width = 100,
|
||||
height = 200,
|
||||
caption = "Love this cool picture! Too bad u cant download it",
|
||||
incrementalMacChunkSize = 0
|
||||
),
|
||||
wasDownloaded = false
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1207,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(
|
||||
@@ -1312,8 +1413,7 @@ class ImportExportTest {
|
||||
expirationTimerMs = 0,
|
||||
muteUntilMs = 0,
|
||||
markedUnread = false,
|
||||
dontNotifyForMentionsIfMuted = false,
|
||||
wallpaper = null
|
||||
dontNotifyForMentionsIfMuted = false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1324,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) }
|
||||
)
|
||||
@@ -1338,7 +1438,7 @@ class ImportExportTest {
|
||||
is Recipient -> writer.write(Frame(recipient = obj))
|
||||
is Chat -> writer.write(Frame(chat = obj))
|
||||
is ChatItem -> writer.write(Frame(chatItem = obj))
|
||||
is Call -> writer.write(Frame(call = obj))
|
||||
is AdHocCall -> writer.write(Frame(adHocCall = obj))
|
||||
is StickerPack -> writer.write(Frame(stickerPack = obj))
|
||||
else -> Assert.fail("invalid object $obj")
|
||||
}
|
||||
@@ -1347,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) }
|
||||
)
|
||||
@@ -1399,18 +1484,19 @@ class ImportExportTest {
|
||||
is Recipient -> writer.write(Frame(recipient = obj))
|
||||
is Chat -> writer.write(Frame(chat = obj))
|
||||
is ChatItem -> writer.write(Frame(chatItem = obj))
|
||||
is Call -> writer.write(Frame(call = obj))
|
||||
is AdHocCall -> writer.write(Frame(adHocCall = obj))
|
||||
is StickerPack -> writer.write(Frame(stickerPack = obj))
|
||||
else -> Assert.fail("invalid object $obj")
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -1430,8 +1516,8 @@ class ImportExportTest {
|
||||
val chatsExported = ArrayList<Chat>()
|
||||
val chatItemsImported = ArrayList<ChatItem>()
|
||||
val chatItemsExported = ArrayList<ChatItem>()
|
||||
val callsImported = ArrayList<Call>()
|
||||
val callsExported = ArrayList<Call>()
|
||||
val callsImported = ArrayList<AdHocCall>()
|
||||
val callsExported = ArrayList<AdHocCall>()
|
||||
val stickersImported = ArrayList<StickerPack>()
|
||||
val stickersExported = ArrayList<StickerPack>()
|
||||
|
||||
@@ -1441,7 +1527,7 @@ class ImportExportTest {
|
||||
f.recipient != null -> recipientsImported.add(f.recipient!!)
|
||||
f.chat != null -> chatsImported.add(f.chat!!)
|
||||
f.chatItem != null -> chatItemsImported.add(f.chatItem!!)
|
||||
f.call != null -> callsImported.add(f.call!!)
|
||||
f.adHocCall != null -> callsImported.add(f.adHocCall!!)
|
||||
f.stickerPack != null -> stickersImported.add(f.stickerPack!!)
|
||||
}
|
||||
}
|
||||
@@ -1449,10 +1535,17 @@ 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.call != null -> callsExported.add(f.call!!)
|
||||
f.adHocCall != null -> callsExported.add(f.adHocCall!!)
|
||||
f.stickerPack != null -> stickersExported.add(f.stickerPack!!)
|
||||
}
|
||||
}
|
||||
@@ -1464,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1477,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)
|
||||
@@ -1500,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>()
|
||||
@@ -1513,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)
|
||||
@@ -1530,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()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
||||
import org.thoughtcrime.securesms.database.MessageType
|
||||
@@ -15,7 +16,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
@@ -137,7 +137,7 @@ class ConversationItemPreviewer {
|
||||
|
||||
private fun attachment(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(
|
||||
ReleaseChannel.CDN_NUMBER,
|
||||
Cdn.CDN_3.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
"image/webp",
|
||||
null,
|
||||
@@ -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
|
||||
|
||||
@@ -328,5 +329,8 @@ class V2ConversationItemShapeTest {
|
||||
override fun onReportSpamLearnMoreClicked() = Unit
|
||||
|
||||
override fun onMessageRequestAcceptOptionsClicked() = Unit
|
||||
|
||||
override fun onItemDoubleClick(item: MultiselectPart) = Unit
|
||||
override fun onPaymentTombstoneClicked() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,9 @@ import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Base64
|
||||
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
|
||||
@@ -24,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
|
||||
@@ -46,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()
|
||||
}
|
||||
@@ -194,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.
|
||||
@@ -219,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
|
||||
@@ -233,6 +240,7 @@ class AttachmentTableTest_deduping {
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
@@ -252,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.
|
||||
@@ -281,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.
|
||||
@@ -296,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.
|
||||
@@ -326,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.
|
||||
@@ -342,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
|
||||
@@ -456,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
|
||||
@@ -469,6 +483,7 @@ class AttachmentTableTest_deduping {
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,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) {
|
||||
@@ -742,7 +766,16 @@ class AttachmentTableTest_deduping {
|
||||
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
|
||||
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
|
||||
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
|
||||
assertEquals(lhsAttachment.cdnNumber, rhsAttachment.cdnNumber)
|
||||
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) {
|
||||
@@ -751,7 +784,7 @@ class AttachmentTableTest_deduping {
|
||||
assertNull(databaseAttachment.remoteLocation)
|
||||
assertNull(databaseAttachment.remoteDigest)
|
||||
assertNull(databaseAttachment.remoteKey)
|
||||
assertEquals(0, databaseAttachment.cdnNumber)
|
||||
assertEquals(0, databaseAttachment.cdn.cdnNumber)
|
||||
}
|
||||
|
||||
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
|
||||
@@ -776,7 +809,7 @@ class AttachmentTableTest_deduping {
|
||||
AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
databaseAttachment.size, // size
|
||||
null,
|
||||
3, // cdnNumber
|
||||
Cdn.CDN_3, // cdnNumber
|
||||
location,
|
||||
key,
|
||||
digest,
|
||||
@@ -791,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)
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
@@ -75,21 +74,6 @@ class GroupTableTest {
|
||||
assertEquals(2, groups.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenGroups_whenIQueryGroupsByMembership_thenIExpectBothGroups() {
|
||||
insertPushGroup()
|
||||
insertMmsGroup(members = listOf(harness.others[1]))
|
||||
|
||||
val groups = groupTable.queryGroupsByMembership(
|
||||
setOf(harness.self.id, harness.others[1]),
|
||||
includeInactive = false,
|
||||
excludeV1 = false,
|
||||
excludeMms = false
|
||||
)
|
||||
|
||||
assertEquals(2, groups.cursor?.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
|
||||
insertPushGroup()
|
||||
@@ -181,72 +165,10 @@ class GroupTableTest {
|
||||
assertFalse(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroup_whenIUpdateMembers_thenIExpectUpdatedMembers() {
|
||||
val v2Group = insertPushGroup()
|
||||
groupTable.updateMembers(v2Group, listOf(harness.self.id, harness.others[1]))
|
||||
val groupRecord = groupTable.getGroup(v2Group)
|
||||
|
||||
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.get().members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnMmsGroup_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||
val members: List<RecipientId> = listOf(harness.self.id, harness.others[0])
|
||||
val other = insertMmsGroup(members + listOf(harness.others[1]))
|
||||
val mmsGroup = insertMmsGroup(members)
|
||||
val actual = groupTable.getOrCreateMmsGroupForMembers(members.toSet())
|
||||
|
||||
assertNotEquals(other, actual)
|
||||
assertEquals(mmsGroup, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleMmsGroups_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1])
|
||||
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2])
|
||||
|
||||
val group1: GroupId = insertMmsGroup(group1Members)
|
||||
val group2: GroupId = insertMmsGroup(group2Members)
|
||||
|
||||
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.toSet())
|
||||
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.toSet())
|
||||
|
||||
assertEquals(group1, group1Result)
|
||||
assertEquals(group2, group2Result)
|
||||
assertNotEquals(group1Result, group2Result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMultipleMmsGroupsWithDifferentMemberOrders_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1], harness.others[2]).shuffled()
|
||||
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2], harness.others[3]).shuffled()
|
||||
|
||||
val group1: GroupId = insertMmsGroup(group1Members)
|
||||
val group2: GroupId = insertMmsGroup(group2Members)
|
||||
|
||||
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.shuffled().toSet())
|
||||
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.shuffled().toSet())
|
||||
|
||||
assertEquals(group1, group1Result)
|
||||
assertEquals(group2, group2Result)
|
||||
assertNotEquals(group1Result, group2Result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMmsGroupWithOneMember_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
|
||||
val groupMembers: List<RecipientId> = listOf(harness.self.id)
|
||||
val group: GroupId = insertMmsGroup(groupMembers)
|
||||
|
||||
val groupResult: GroupId = groupTable.getOrCreateMmsGroupForMembers(groupMembers.toSet())
|
||||
|
||||
assertEquals(group, groupResult)
|
||||
}
|
||||
|
||||
@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)
|
||||
@@ -273,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))
|
||||
@@ -292,6 +293,7 @@ class GroupTableTest {
|
||||
}
|
||||
|
||||
private fun insertPushGroup(
|
||||
title: String = "Test Group",
|
||||
members: List<DecryptedMember> = listOf(
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(harness.self.requireAci().toByteString())
|
||||
@@ -307,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
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIsSize
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class NameCollisionTablesTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private lateinit var bob: RecipientId
|
||||
private lateinit var charlie: RecipientId
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
alice = setUpRecipient(harness.others[0])
|
||||
bob = setUpRecipient(harness.others[1])
|
||||
charlie = setUpRecipient(harness.others[2])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAUserWithAThreadIdButNoConflicts_whenIGetCollisionsForThreadRecipient_thenIExpectNoCollisions() {
|
||||
val threadRecipientId = alice
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(threadRecipientId))
|
||||
val actual = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(threadRecipientId)
|
||||
|
||||
actual assertIsSize 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoUsers_whenOneChangesTheirProfileNameToMatchTheOther_thenIExpectANameCollision() {
|
||||
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
|
||||
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||
|
||||
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
|
||||
|
||||
actualAlice assertIsSize 2
|
||||
actualBob assertIsSize 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectNoNameCollisions() {
|
||||
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
|
||||
|
||||
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
|
||||
|
||||
actualAlice assertIsSize 0
|
||||
actualBob assertIsSize 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenThreeUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectTwoNameCollisions() {
|
||||
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(charlie, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
|
||||
|
||||
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
|
||||
val actualCharlie = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(charlie)
|
||||
|
||||
actualAlice assertIsSize 0
|
||||
actualBob assertIsSize 2
|
||||
actualCharlie assertIsSize 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoUsersWithADismissedNameCollision_whenOneChangesToADifferentNameAndBack_thenIExpectANameCollision() {
|
||||
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
|
||||
|
||||
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
|
||||
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||
|
||||
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||
|
||||
actualAlice assertIsSize 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
|
||||
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
|
||||
|
||||
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||
|
||||
actualCollisions assertIsSize 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADismissedNameCollisionForAliceThatIUpdate_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
|
||||
|
||||
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
|
||||
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||
|
||||
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
|
||||
|
||||
actualCollisions assertIsSize 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForBob_thenIExpectANameCollisionWithTwoEntries() {
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
|
||||
|
||||
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
|
||||
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
|
||||
|
||||
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
|
||||
|
||||
actualCollisions assertIsSize 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
|
||||
val alice = Recipient.resolved(alice)
|
||||
val bob = Recipient.resolved(bob)
|
||||
val info = createGroup()
|
||||
|
||||
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
|
||||
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
|
||||
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
|
||||
|
||||
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
|
||||
|
||||
collisions assertIsSize 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupWithAliceAndBobWithDismissedCollision_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
|
||||
val alice = Recipient.resolved(alice)
|
||||
val bob = Recipient.resolved(bob)
|
||||
val info = createGroup()
|
||||
|
||||
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
|
||||
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
|
||||
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
|
||||
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
|
||||
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(info.recipientId)
|
||||
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
|
||||
|
||||
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
|
||||
|
||||
collisions assertIsSize 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAliceWithMismatch_thenIExpectNoGroupNameCollision() {
|
||||
val alice = Recipient.resolved(alice)
|
||||
val bob = Recipient.resolved(bob)
|
||||
val info = createGroup()
|
||||
|
||||
setProfileName(alice.id, ProfileName.fromParts("Alice", "Android"))
|
||||
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
|
||||
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
|
||||
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Alice Android", "Bob Android")
|
||||
|
||||
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
|
||||
|
||||
collisions assertIsSize 0
|
||||
}
|
||||
|
||||
private fun setUpRecipient(recipientId: RecipientId): RecipientId {
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, false)
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
|
||||
|
||||
MmsHelper.insert(
|
||||
threadId = threadId,
|
||||
message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = recipientId,
|
||||
groupId = null,
|
||||
body = "hi",
|
||||
sentTimeMillis = 100L,
|
||||
receivedTimeMillis = 200L,
|
||||
serverTimeMillis = 100L,
|
||||
isUnidentified = true
|
||||
)
|
||||
)
|
||||
|
||||
return recipientId
|
||||
}
|
||||
|
||||
private fun setProfileName(recipientId: RecipientId, name: ProfileName) {
|
||||
SignalDatabase.recipients.setProfileName(recipientId, name)
|
||||
SignalDatabase.nameCollisions.handleIndividualNameCollision(recipientId)
|
||||
}
|
||||
|
||||
private fun createGroup(): GroupTestingUtils.TestGroupInfo {
|
||||
return GroupTestingUtils.insertGroup(
|
||||
revision = 0,
|
||||
DecryptedMember(
|
||||
aciBytes = harness.self.requireAci().toByteString(),
|
||||
role = Member.Role.ADMINISTRATOR
|
||||
),
|
||||
DecryptedMember(
|
||||
aciBytes = Recipient.resolved(alice).requireAci().toByteString(),
|
||||
role = Member.Role.ADMINISTRATOR
|
||||
),
|
||||
DecryptedMember(
|
||||
aciBytes = Recipient.resolved(bob).requireAci().toByteString(),
|
||||
role = Member.Role.ADMINISTRATOR
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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, 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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.signal.benchmark.setup
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Cdn
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.MessageType
|
||||
@@ -9,7 +10,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
@@ -144,7 +144,7 @@ object TestMessages {
|
||||
}
|
||||
private fun imageAttachment(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(
|
||||
ReleaseChannel.CDN_NUMBER,
|
||||
Cdn.S3.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
"image/webp",
|
||||
null,
|
||||
@@ -161,13 +161,14 @@ object TestMessages {
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
System.currentTimeMillis()
|
||||
System.currentTimeMillis(),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun voiceAttachment(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(
|
||||
ReleaseChannel.CDN_NUMBER,
|
||||
Cdn.S3.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
"audio/aac",
|
||||
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"))
|
||||
|
||||
@@ -104,7 +104,7 @@ object TestUsers {
|
||||
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
|
||||
)
|
||||
|
||||
@@ -67,7 +67,8 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
hasWallpaper = springboardViewModel.hasWallpaper.value,
|
||||
colorizer = Colorizer(),
|
||||
startExpirationTimeout = {},
|
||||
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) }
|
||||
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) },
|
||||
displayDialogFragment = {}
|
||||
)
|
||||
|
||||
if (springboardViewModel.hasWallpaper.value) {
|
||||
@@ -279,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()
|
||||
}
|
||||
|
||||
@@ -299,6 +300,14 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onItemDoubleClick(item: MultiselectPart) {
|
||||
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.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,7 +816,14 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".registration.RegistrationNavigationActivity"
|
||||
<activity android:name=".registration.ui.RegistrationActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".restore.RestoreActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
@@ -933,12 +926,18 @@
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
|
||||
<intent-filter>
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.videocall" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".mediasend.AvatarSelectionActivity"
|
||||
@@ -962,8 +961,8 @@
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".backup.v2.ui.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"
|
||||
@@ -1079,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"/>
|
||||
@@ -1094,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"
|
||||
@@ -1168,6 +1167,10 @@
|
||||
android:name=".service.AttachmentProgressService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".service.BackupProgressService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".gcm.FcmFetchBackgroundService"
|
||||
android:exported="false"/>
|
||||
@@ -1288,6 +1291,12 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
|
||||
|
||||
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
|
||||
@@ -1352,7 +1361,11 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService" android:exported="false" />
|
||||
<service
|
||||
android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="camera|microphone" />
|
||||
|
||||
<receiver android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallServiceReceiver" android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.DENY"/>
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,26 +40,29 @@ import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
|
||||
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
|
||||
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;
|
||||
@@ -69,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;
|
||||
@@ -83,18 +85,20 @@ import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil;
|
||||
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
|
||||
import org.thoughtcrime.securesms.service.AnalyzeDatabaseAlarmListener;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.service.MessageBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.webrtc.ActiveCallManager;
|
||||
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
|
||||
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;
|
||||
@@ -140,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);
|
||||
}
|
||||
|
||||
@@ -165,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", () -> {
|
||||
@@ -175,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())
|
||||
@@ -193,30 +197,31 @@ 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)
|
||||
.addPostRender(() -> ActiveCallManager.clearNotifications(this))
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -229,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();
|
||||
|
||||
@@ -251,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);
|
||||
@@ -264,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,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;
|
||||
});
|
||||
@@ -309,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();
|
||||
@@ -350,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() {
|
||||
@@ -382,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,8 +425,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
RotateSignedPreKeyListener.schedule(this);
|
||||
DirectoryRefreshListener.schedule(this);
|
||||
LocalBackupListener.schedule(this);
|
||||
MessageBackupListener.schedule(this);
|
||||
RotateSenderCertificateListener.schedule(this);
|
||||
RoutineMessageFetchReceiver.startOrUpdateAlarm(this);
|
||||
AnalyzeDatabaseAlarmListener.schedule(this);
|
||||
|
||||
if (BuildConfig.MANAGES_APP_UPDATES) {
|
||||
ApkUpdateRefreshListener.schedule(this);
|
||||
@@ -428,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);
|
||||
@@ -442,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) {
|
||||
@@ -452,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -58,6 +59,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
|
||||
void setEventListener(@Nullable EventListener listener);
|
||||
|
||||
default void setGestureDetector(@Nullable GestureDetector gestureDetector) {
|
||||
// Intentionally Blank.
|
||||
}
|
||||
|
||||
default void setParentScrolling(boolean isParentScrolling) {
|
||||
// Intentionally Blank.
|
||||
}
|
||||
@@ -120,11 +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;
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
|
||||
@@ -24,6 +25,8 @@ class ContactSelectionListAdapter(
|
||||
init {
|
||||
registerFactory(NewGroupModel::class.java, LayoutFactory({ NewGroupViewHolder(it, onClickCallbacks::onNewGroupClicked) }, R.layout.contact_selection_new_group_item))
|
||||
registerFactory(InviteToSignalModel::class.java, LayoutFactory({ InviteToSignalViewHolder(it, onClickCallbacks::onInviteToSignalClicked) }, R.layout.contact_selection_invite_action_item))
|
||||
registerFactory(FindContactsModel::class.java, LayoutFactory({ FindContactsViewHolder(it, onClickCallbacks::onFindContactsClicked) }, R.layout.contact_selection_find_contacts_item))
|
||||
registerFactory(FindContactsBannerModel::class.java, LayoutFactory({ FindContactsBannerViewHolder(it, onClickCallbacks::onDismissFindContactsBannerClicked, onClickCallbacks::onFindContactsClicked) }, R.layout.contact_selection_find_contacts_banner_item))
|
||||
registerFactory(RefreshContactsModel::class.java, LayoutFactory({ RefreshContactsViewHolder(it, onClickCallbacks::onRefreshContactsClicked) }, R.layout.contact_selection_refresh_action_item))
|
||||
registerFactory(MoreHeaderModel::class.java, LayoutFactory({ MoreHeaderViewHolder(it) }, R.layout.contact_search_section_header))
|
||||
registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.contact_selection_empty_state))
|
||||
@@ -46,6 +49,16 @@ class ContactSelectionListAdapter(
|
||||
override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true
|
||||
}
|
||||
|
||||
class FindContactsModel : MappingModel<FindContactsModel> {
|
||||
override fun areItemsTheSame(newItem: FindContactsModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: FindContactsModel): Boolean = true
|
||||
}
|
||||
|
||||
class FindContactsBannerModel : MappingModel<FindContactsBannerModel> {
|
||||
override fun areItemsTheSame(newItem: FindContactsBannerModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: FindContactsBannerModel): Boolean = true
|
||||
}
|
||||
|
||||
class FindByUsernameModel : MappingModel<FindByUsernameModel> {
|
||||
override fun areItemsTheSame(newItem: FindByUsernameModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: FindByUsernameModel): Boolean = true
|
||||
@@ -86,6 +99,23 @@ class ContactSelectionListAdapter(
|
||||
override fun bind(model: RefreshContactsModel) = Unit
|
||||
}
|
||||
|
||||
private class FindContactsViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<FindContactsModel>(itemView) {
|
||||
init {
|
||||
itemView.setOnClickListener { onClickListener() }
|
||||
}
|
||||
|
||||
override fun bind(model: FindContactsModel) = Unit
|
||||
}
|
||||
|
||||
private class FindContactsBannerViewHolder(itemView: View, onDismissListener: () -> Unit, onClickListener: () -> Unit) : MappingViewHolder<FindContactsBannerModel>(itemView) {
|
||||
init {
|
||||
itemView.findViewById<MaterialButton>(R.id.no_thanks_button).setOnClickListener { onDismissListener() }
|
||||
itemView.findViewById<MaterialButton>(R.id.allow_contacts_button).setOnClickListener { onClickListener() }
|
||||
}
|
||||
|
||||
override fun bind(model: FindContactsBannerModel) = Unit
|
||||
}
|
||||
|
||||
private class MoreHeaderViewHolder(itemView: View) : MappingViewHolder<MoreHeaderModel>(itemView) {
|
||||
|
||||
private val headerTextView: TextView = itemView.findViewById(R.id.section_header)
|
||||
@@ -129,6 +159,8 @@ class ContactSelectionListAdapter(
|
||||
INVITE_TO_SIGNAL("invite-to-signal"),
|
||||
MORE_HEADING("more-heading"),
|
||||
REFRESH_CONTACTS("refresh-contacts"),
|
||||
FIND_CONTACTS("find-contacts"),
|
||||
FIND_CONTACTS_BANNER("find-contacts-banner"),
|
||||
FIND_BY_USERNAME("find-by-username"),
|
||||
FIND_BY_PHONE_NUMBER("find-by-phone-number");
|
||||
|
||||
@@ -152,6 +184,8 @@ class ContactSelectionListAdapter(
|
||||
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
|
||||
ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
|
||||
ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
|
||||
ArbitraryRow.FIND_CONTACTS -> FindContactsModel()
|
||||
ArbitraryRow.FIND_CONTACTS_BANNER -> FindContactsBannerModel()
|
||||
ArbitraryRow.FIND_BY_PHONE_NUMBER -> FindByPhoneNumberModel()
|
||||
ArbitraryRow.FIND_BY_USERNAME -> FindByUsernameModel()
|
||||
}
|
||||
@@ -162,6 +196,8 @@ class ContactSelectionListAdapter(
|
||||
fun onNewGroupClicked()
|
||||
fun onInviteToSignalClicked()
|
||||
fun onRefreshContactsClicked()
|
||||
fun onFindContactsClicked()
|
||||
fun onDismissFindContactsBannerClicked()
|
||||
fun onFindByPhoneNumberClicked()
|
||||
fun onFindByUsernameClicked()
|
||||
}
|
||||
|
||||
@@ -70,13 +70,13 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameAciFetchResult;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -125,10 +125,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
private TextView emptyText;
|
||||
private OnContactSelectedListener onContactSelectedListener;
|
||||
private SwipeRefreshLayout swipeRefresh;
|
||||
private View showContactsLayout;
|
||||
private Button showContactsButton;
|
||||
private TextView showContactsDescription;
|
||||
private ProgressWheel showContactsProgress;
|
||||
private String cursorFilter;
|
||||
private RecyclerView recyclerView;
|
||||
private RecyclerViewFastScroller fastScroller;
|
||||
@@ -223,43 +219,25 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
||||
handleContactPermissionGranted();
|
||||
} else {
|
||||
contactSearchMediator.refresh();
|
||||
}
|
||||
})
|
||||
.onAnyDenied(() -> {
|
||||
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
|
||||
if (safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false))) {
|
||||
contactSearchMediator.refresh();
|
||||
} else {
|
||||
initializeNoContactsPermission();
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
if (hasContactsPermissions(requireContext()) && !TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
||||
handleContactPermissionGranted();
|
||||
} else {
|
||||
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
contactSearchMediator.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
|
||||
|
||||
emptyText = view.findViewById(android.R.id.empty);
|
||||
recyclerView = view.findViewById(R.id.recycler_view);
|
||||
swipeRefresh = view.findViewById(R.id.swipe_refresh);
|
||||
fastScroller = view.findViewById(R.id.fast_scroller);
|
||||
showContactsLayout = view.findViewById(R.id.show_contacts_container);
|
||||
showContactsButton = view.findViewById(R.id.show_contacts_button);
|
||||
showContactsDescription = view.findViewById(R.id.show_contacts_description);
|
||||
showContactsProgress = view.findViewById(R.id.progress);
|
||||
chipRecycler = view.findViewById(R.id.chipRecycler);
|
||||
constraintLayout = view.findViewById(R.id.container);
|
||||
headerActionView = view.findViewById(R.id.header_action);
|
||||
emptyText = view.findViewById(android.R.id.empty);
|
||||
recyclerView = view.findViewById(R.id.recycler_view);
|
||||
swipeRefresh = view.findViewById(R.id.swipe_refresh);
|
||||
fastScroller = view.findViewById(R.id.fast_scroller);
|
||||
chipRecycler = view.findViewById(R.id.chipRecycler);
|
||||
constraintLayout = view.findViewById(R.id.container);
|
||||
headerActionView = view.findViewById(R.id.header_action);
|
||||
|
||||
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
|
||||
|
||||
@@ -269,6 +247,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
recyclerView.setAlpha(1f);
|
||||
}
|
||||
});
|
||||
|
||||
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
|
||||
@@ -354,7 +337,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
selectionLimit,
|
||||
new ContactSearchAdapter.DisplayOptions(
|
||||
isMulti,
|
||||
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
|
||||
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
|
||||
newCallCallback != null,
|
||||
false
|
||||
@@ -372,6 +354,19 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
fixedContacts,
|
||||
displayOptions,
|
||||
new ContactSelectionListAdapter.OnContactSelectionClick() {
|
||||
@Override
|
||||
public void onDismissFindContactsBannerClicked() {
|
||||
SignalStore.uiHints().markDismissedContactsPermissionBanner();
|
||||
if (onRefreshListener != null) {
|
||||
onRefreshListener.onRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindContactsClicked() {
|
||||
requestContactPermissions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefreshContactsClicked() {
|
||||
if (onRefreshListener != null) {
|
||||
@@ -498,6 +493,27 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
return isMulti;
|
||||
}
|
||||
|
||||
private void requestContactPermissions() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
|
||||
.ifNecessary()
|
||||
.onAllGranted(() -> {
|
||||
recyclerView.setAlpha(0.5f);
|
||||
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
|
||||
handleContactPermissionGranted();
|
||||
} else {
|
||||
contactSearchMediator.refresh();
|
||||
if (onRefreshListener != null) {
|
||||
swipeRefresh.setRefreshing(true);
|
||||
onRefreshListener.onRefresh();
|
||||
}
|
||||
}
|
||||
})
|
||||
.onAnyDenied(() -> contactSearchMediator.refresh())
|
||||
.withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts), null, R.string.ContactSelectionListFragment_allow_access_contacts, R.string.ContactSelectionListFragment_to_find_people, getParentFragmentManager())
|
||||
.execute();
|
||||
}
|
||||
|
||||
private void initializeCursor() {
|
||||
recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
|
||||
recyclerView.setAdapter(contactSearchMediator.getAdapter());
|
||||
@@ -521,28 +537,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
return hasQueryFilter() || shouldDisplayRecents();
|
||||
}
|
||||
|
||||
private void initializeNoContactsPermission() {
|
||||
swipeRefresh.setVisibility(View.GONE);
|
||||
|
||||
showContactsLayout.setVisibility(View.VISIBLE);
|
||||
showContactsProgress.setVisibility(View.INVISIBLE);
|
||||
showContactsDescription.setText(R.string.contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them);
|
||||
showContactsButton.setVisibility(View.VISIBLE);
|
||||
|
||||
showContactsButton.setOnClickListener(v -> {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts))
|
||||
.onSomeGranted(permissions -> {
|
||||
if (permissions.contains(Manifest.permission.WRITE_CONTACTS)) {
|
||||
handleContactPermissionGranted();
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
public void setQueryFilter(String filter) {
|
||||
if (Objects.equals(filter, this.cursorFilter)) {
|
||||
return;
|
||||
@@ -583,7 +577,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
showContactsLayout.setVisibility(View.GONE);
|
||||
|
||||
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
|
||||
boolean useFastScroller = count > 20;
|
||||
@@ -614,12 +607,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
new AsyncTask<Void, Void, Boolean>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
swipeRefresh.setVisibility(View.GONE);
|
||||
showContactsLayout.setVisibility(View.VISIBLE);
|
||||
showContactsButton.setVisibility(View.INVISIBLE);
|
||||
showContactsDescription.setText(R.string.ConversationListFragment_loading);
|
||||
showContactsProgress.setVisibility(View.VISIBLE);
|
||||
showContactsProgress.spin();
|
||||
if (onRefreshListener != null) {
|
||||
setRefreshing(true);
|
||||
onRefreshListener.onRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -636,14 +627,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
if (result) {
|
||||
showContactsLayout.setVisibility(View.GONE);
|
||||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
reset();
|
||||
} else {
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
|
||||
initializeNoContactsPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -890,6 +878,13 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
return ContactSearchConfiguration.build(builder -> {
|
||||
builder.setQuery(contactSearchState.getQuery());
|
||||
|
||||
if (newConversationCallback != null &&
|
||||
!hasContactsPermissions(requireContext()) &&
|
||||
!SignalStore.uiHints().getDismissedContactsPermissionBanner() &&
|
||||
!hasQuery) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_CONTACTS_BANNER.getCode());
|
||||
}
|
||||
|
||||
if (newConversationCallback != null && !hasQuery) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
|
||||
}
|
||||
@@ -946,7 +941,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
builder.username(newRowMode);
|
||||
}
|
||||
|
||||
if ((newCallCallback != null || newConversationCallback != null) && !hasQuery) {
|
||||
if ((newCallCallback != null || newConversationCallback != null)) {
|
||||
addMoreSection(builder);
|
||||
builder.withEmptyState(emptyBuilder -> {
|
||||
emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
|
||||
@@ -959,9 +954,17 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
});
|
||||
}
|
||||
|
||||
private boolean hasContactsPermissions(@NonNull Context context) {
|
||||
return Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS);
|
||||
}
|
||||
|
||||
private void addMoreSection(@NonNull ContactSearchConfiguration.Builder builder) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.MORE_HEADING.getCode());
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode());
|
||||
if (hasContactsPermissions(requireContext())) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode());
|
||||
} else if (SignalStore.uiHints().getDismissedContactsPermissionBanner()) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_CONTACTS.getCode());
|
||||
}
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -131,14 +131,15 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
|
||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_scan_qr_code_allow_camera), R.drawable.symbol_camera_24)
|
||||
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getSupportFragmentManager())
|
||||
.onAllGranted(() -> {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.fragment_container, deviceAddFragment)
|
||||
.addToBackStack(null)
|
||||
.commitAllowingStateLoss();
|
||||
})
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.DeviceActivity_unable_to_scan_a_qr_code_without_the_camera_permission, Toast.LENGTH_LONG).show())
|
||||
.onAnyDenied(() -> Toast.makeText(this, R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show())
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -190,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
|
||||
|
||||
@@ -136,7 +136,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
|
||||
if (result instanceof RecipientRepository.LookupResult.Success) {
|
||||
Recipient resolved = Recipient.resolved(((RecipientRepository.LookupResult.Success) result).getRecipientId());
|
||||
if (resolved.isRegistered() && resolved.hasServiceId()) {
|
||||
if (resolved.isRegistered() && resolved.getHasServiceId()) {
|
||||
launch(resolved);
|
||||
}
|
||||
} else if (result instanceof RecipientRepository.LookupResult.NotFound || result instanceof RecipientRepository.LookupResult.InvalidEntry) {
|
||||
@@ -334,13 +334,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
R.drawable.ic_minus_circle_20, // TODO [alex] -- correct asset
|
||||
getString(R.string.NewConversationActivity__remove),
|
||||
R.color.signal_colorOnSurface,
|
||||
() -> {
|
||||
if (recipient.isSystemContact()) {
|
||||
displayIsInSystemContactsDialog(recipient);
|
||||
} else {
|
||||
displayRemovalDialog(recipient);
|
||||
}
|
||||
}
|
||||
() -> displayRemovalDialog(recipient)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ 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.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
|
||||
@@ -27,9 +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.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.RemoteConfig;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -51,6 +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_TRANSFER_OR_RESTORE = 11;
|
||||
|
||||
private SignalServiceNetworkAccess networkAccess;
|
||||
private BroadcastReceiver clearKeyReceiver;
|
||||
@@ -59,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);
|
||||
@@ -88,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();
|
||||
}
|
||||
|
||||
@@ -125,8 +128,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private void routeApplicationState(boolean locked) {
|
||||
Intent intent = getIntentForState(getApplicationState(locked));
|
||||
final int applicationState = getApplicationState(locked);
|
||||
Intent intent = getIntentForState(applicationState);
|
||||
if (intent != null) {
|
||||
Log.d(TAG, "routeApplicationState(), intent: " + intent.getComponent());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
@@ -146,6 +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_TRANSFER_OR_RESTORE: return getTransferOrRestoreIntent();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
@@ -163,6 +169,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
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()) {
|
||||
@@ -178,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() {
|
||||
@@ -196,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;
|
||||
}
|
||||
|
||||
@@ -208,7 +220,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
}
|
||||
|
||||
private Intent getPushRegistrationIntent() {
|
||||
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
return RegistrationActivity.newIntentForNewRegistration(this, getIntent());
|
||||
}
|
||||
|
||||
private Intent getEnterSignalPinIntent() {
|
||||
@@ -227,6 +239,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
|
||||
return getRoutedIntent(CreateSvrPinActivity.class, intent);
|
||||
}
|
||||
|
||||
private Intent getTransferOrRestoreIntent() {
|
||||
Intent intent = RestoreActivity.getIntentForTransferOrRestore(this);
|
||||
return getRoutedIntent(intent, getIntent());
|
||||
}
|
||||
|
||||
private Intent getCreateProfileNameIntent() {
|
||||
Intent intent = CreateProfileActivity.getIntentForUserProfile(this);
|
||||
return getRoutedIntent(intent, getIntent());
|
||||
|
||||
@@ -29,8 +29,10 @@ 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;
|
||||
@@ -61,6 +63,7 @@ import org.thoughtcrime.securesms.components.webrtc.CallLinkProfileKeySender;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallReactionScrubber;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
|
||||
@@ -78,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;
|
||||
@@ -91,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;
|
||||
@@ -114,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 {
|
||||
|
||||
@@ -162,7 +166,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
private ControlsAndInfoController controlsAndInfo;
|
||||
private boolean enterPipOnResume;
|
||||
private long lastProcessedIntentTimestamp;
|
||||
|
||||
private WebRtcViewModel previousEvent = null;
|
||||
private boolean isAskingForPermission;
|
||||
private Disposable ephemeralStateDisposable = Disposable.empty();
|
||||
|
||||
@Override
|
||||
@@ -222,6 +227,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
processIntent(getIntent());
|
||||
|
||||
registerSystemPipChangeListeners();
|
||||
|
||||
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
|
||||
|
||||
windowInfoTrackerCallbackAdapter = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
|
||||
@@ -232,16 +239,29 @@ 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() {
|
||||
addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfo -> {
|
||||
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
|
||||
CallReactionScrubber.dismissCustomEmojiBottomSheet(getSupportFragmentManager());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
@@ -287,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();
|
||||
@@ -307,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -356,8 +376,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
return false;
|
||||
}
|
||||
|
||||
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
|
||||
|
||||
return true;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
@@ -397,7 +415,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
}
|
||||
|
||||
private void initializePendingParticipantFragmentListener() {
|
||||
if (!FeatureFlags.adHocCalling()) {
|
||||
if (!RemoteConfig.adHocCalling()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -422,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();
|
||||
@@ -434,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();
|
||||
@@ -502,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 -> {
|
||||
@@ -633,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));
|
||||
@@ -719,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) {
|
||||
@@ -817,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) {
|
||||
@@ -847,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());
|
||||
}
|
||||
@@ -861,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();
|
||||
@@ -885,7 +886,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEventMainThread(@NonNull WebRtcViewModel event) {
|
||||
Log.i(TAG, "Got message from service: " + event);
|
||||
Log.i(TAG, "Got message from service: " + event.describeDifference(previousEvent));
|
||||
previousEvent = event;
|
||||
|
||||
viewModel.setRecipient(event.getRecipient());
|
||||
callScreen.setRecipient(event.getRecipient());
|
||||
@@ -980,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();
|
||||
@@ -1002,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();
|
||||
}
|
||||
|
||||
@@ -1021,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);
|
||||
@@ -1051,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
|
||||
@@ -1061,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
|
||||
@@ -1114,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);
|
||||
}
|
||||
}
|
||||
@@ -1147,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
|
||||
@@ -1198,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,6 +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 restore complete.")
|
||||
}
|
||||
|
||||
private fun cumulativeHashCode(): Int {
|
||||
@@ -61,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;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.attachments
|
||||
|
||||
import android.net.Uri
|
||||
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 {
|
||||
|
||||
@JvmField
|
||||
val archiveCdn: Int
|
||||
|
||||
@JvmField
|
||||
val archiveMediaName: String
|
||||
|
||||
@JvmField
|
||||
val archiveMediaId: String
|
||||
|
||||
@JvmField
|
||||
val archiveThumbnailMediaId: String
|
||||
|
||||
constructor(
|
||||
contentType: String?,
|
||||
size: Long,
|
||||
cdn: Int,
|
||||
key: ByteArray,
|
||||
cdnKey: String?,
|
||||
archiveCdn: Int?,
|
||||
archiveMediaName: String,
|
||||
archiveMediaId: String,
|
||||
archiveThumbnailMediaId: String,
|
||||
digest: ByteArray,
|
||||
incrementalMac: ByteArray?,
|
||||
incrementalMacChunkSize: Int?,
|
||||
width: Int?,
|
||||
height: Int?,
|
||||
caption: String?,
|
||||
blurHash: String?,
|
||||
voiceNote: Boolean,
|
||||
borderless: Boolean,
|
||||
stickerLocator: StickerLocator?,
|
||||
gif: Boolean,
|
||||
quote: Boolean,
|
||||
uuid: UUID?
|
||||
) : super(
|
||||
contentType = contentType ?: "",
|
||||
quote = quote,
|
||||
transferState = AttachmentTable.TRANSFER_NEEDS_RESTORE,
|
||||
size = size,
|
||||
fileName = null,
|
||||
cdn = Cdn.fromCdnNumber(cdn),
|
||||
remoteLocation = cdnKey,
|
||||
remoteKey = Base64.encodeWithoutPadding(key),
|
||||
remoteDigest = digest,
|
||||
incrementalDigest = incrementalMac,
|
||||
fastPreflightId = null,
|
||||
voiceNote = voiceNote,
|
||||
borderless = borderless,
|
||||
videoGif = gif,
|
||||
width = width ?: 0,
|
||||
height = height ?: 0,
|
||||
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
|
||||
uploadTimestamp = 0,
|
||||
caption = caption,
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = BlurHash.parseOrNull(blurHash),
|
||||
audioHash = null,
|
||||
transformProperties = null,
|
||||
uuid = uuid
|
||||
) {
|
||||
this.archiveCdn = archiveCdn ?: Cdn.CDN_3.cdnNumber
|
||||
this.archiveMediaName = archiveMediaName
|
||||
this.archiveMediaId = archiveMediaId
|
||||
this.archiveThumbnailMediaId = archiveThumbnailMediaId
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
archiveCdn = parcel.readInt()
|
||||
archiveMediaName = parcel.readString()!!
|
||||
archiveMediaId = parcel.readString()!!
|
||||
archiveThumbnailMediaId = parcel.readString()!!
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
super.writeToParcel(dest, flags)
|
||||
dest.writeInt(archiveCdn)
|
||||
dest.writeString(archiveMediaName)
|
||||
dest.writeString(archiveMediaId)
|
||||
dest.writeString(archiveThumbnailMediaId)
|
||||
}
|
||||
|
||||
override val uri: Uri? = null
|
||||
override val publicUri: Uri? = null
|
||||
override val thumbnailUri: Uri? = null
|
||||
}
|
||||
@@ -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
|
||||
@@ -29,7 +31,7 @@ abstract class Attachment(
|
||||
@JvmField
|
||||
val fileName: String?,
|
||||
@JvmField
|
||||
val cdnNumber: Int,
|
||||
val cdn: Cdn,
|
||||
@JvmField
|
||||
val remoteLocation: String?,
|
||||
@JvmField
|
||||
@@ -65,18 +67,23 @@ 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()!!,
|
||||
transferState = parcel.readInt(),
|
||||
size = parcel.readLong(),
|
||||
fileName = parcel.readString(),
|
||||
cdnNumber = parcel.readInt(),
|
||||
cdn = Cdn.deserialize(parcel.readInt()),
|
||||
remoteLocation = parcel.readString(),
|
||||
remoteKey = parcel.readString(),
|
||||
remoteDigest = ParcelUtil.readByteArray(parcel),
|
||||
@@ -94,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) {
|
||||
@@ -103,7 +111,7 @@ abstract class Attachment(
|
||||
dest.writeInt(transferState)
|
||||
dest.writeLong(size)
|
||||
dest.writeString(fileName)
|
||||
dest.writeInt(cdnNumber)
|
||||
dest.writeInt(cdn.serialize())
|
||||
dest.writeString(remoteLocation)
|
||||
dest.writeString(remoteKey)
|
||||
ParcelUtil.writeByteArray(dest, remoteDigest)
|
||||
@@ -122,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 {
|
||||
@@ -129,7 +138,7 @@ abstract class Attachment(
|
||||
}
|
||||
|
||||
val isInProgress: Boolean
|
||||
get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
|
||||
get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE && transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED
|
||||
|
||||
val isPermanentlyFailed: Boolean
|
||||
get() = transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
|
||||
|
||||
@@ -17,7 +17,8 @@ object AttachmentCreator : Parcelable.Creator<Attachment> {
|
||||
DATABASE(DatabaseAttachment::class.java, "database"),
|
||||
POINTER(PointerAttachment::class.java, "pointer"),
|
||||
TOMBSTONE(TombstoneAttachment::class.java, "tombstone"),
|
||||
URI(UriAttachment::class.java, "uri")
|
||||
URI(UriAttachment::class.java, "uri"),
|
||||
ARCHIVED(ArchivedAttachment::class.java, "archived")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -34,6 +35,7 @@ object AttachmentCreator : Parcelable.Creator<Attachment> {
|
||||
Subclass.POINTER -> PointerAttachment(source)
|
||||
Subclass.TOMBSTONE -> TombstoneAttachment(source)
|
||||
Subclass.URI -> UriAttachment(source)
|
||||
Subclass.ARCHIVED -> ArchivedAttachment(source)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.attachments
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.protos.resumableuploads.ResumableUpload
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHashEncoder
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream
|
||||
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
|
||||
import java.io.IOException
|
||||
import java.util.Objects
|
||||
|
||||
/**
|
||||
* A place collect common attachment upload operations to allow for code reuse.
|
||||
*/
|
||||
object AttachmentUploadUtil {
|
||||
|
||||
private val TAG = Log.tag(AttachmentUploadUtil::class.java)
|
||||
|
||||
/**
|
||||
* Builds a [SignalServiceAttachmentStream] from the provided data, which can then be provided to various upload methods.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun buildSignalServiceAttachmentStream(
|
||||
context: Context,
|
||||
attachment: Attachment,
|
||||
uploadSpec: ResumableUpload,
|
||||
cancellationSignal: (() -> Boolean)? = null,
|
||||
progressListener: ProgressListener? = null
|
||||
): SignalServiceAttachmentStream {
|
||||
val inputStream = PartAuthority.getAttachmentStream(context, attachment.uri!!)
|
||||
val builder = SignalServiceAttachment.newStreamBuilder()
|
||||
.withStream(inputStream)
|
||||
.withContentType(attachment.contentType)
|
||||
.withLength(attachment.size)
|
||||
.withFileName(attachment.fileName)
|
||||
.withVoiceNote(attachment.voiceNote)
|
||||
.withBorderless(attachment.borderless)
|
||||
.withGif(attachment.videoGif)
|
||||
.withFaststart(attachment.transformProperties?.mp4FastStart ?: false)
|
||||
.withWidth(attachment.width)
|
||||
.withHeight(attachment.height)
|
||||
.withUploadTimestamp(System.currentTimeMillis())
|
||||
.withCaption(attachment.caption)
|
||||
.withResumableUploadSpec(ResumableUploadSpec.from(uploadSpec))
|
||||
.withCancelationSignal(cancellationSignal)
|
||||
.withListener(progressListener)
|
||||
.withUuid(attachment.uuid)
|
||||
|
||||
if (MediaUtil.isImageType(attachment.contentType)) {
|
||||
builder.withBlurHash(getImageBlurHash(context, attachment))
|
||||
} else if (MediaUtil.isVideoType(attachment.contentType)) {
|
||||
builder.withBlurHash(getVideoBlurHash(context, attachment))
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun getImageBlurHash(context: Context, attachment: Attachment): String? {
|
||||
if (attachment.blurHash != null) {
|
||||
return attachment.blurHash!!.hash
|
||||
}
|
||||
|
||||
if (attachment.uri == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return PartAuthority.getAttachmentStream(context, attachment.uri!!).use { inputStream ->
|
||||
BlurHashEncoder.encode(inputStream)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun getVideoBlurHash(context: Context, attachment: Attachment): String? {
|
||||
if (attachment.blurHash != null) {
|
||||
return attachment.blurHash.hash
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
Log.w(TAG, "Video thumbnails not supported...")
|
||||
return null
|
||||
}
|
||||
|
||||
return MediaUtil.getVideoThumbnail(context, Objects.requireNonNull(attachment.uri), 1000)?.let { bitmap ->
|
||||
val thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false)
|
||||
bitmap.recycle()
|
||||
|
||||
Log.i(TAG, "Generated video thumbnail...")
|
||||
val hash = BlurHashEncoder.encode(thumb)
|
||||
thumb.recycle()
|
||||
|
||||
hash
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.attachments
|
||||
|
||||
import org.signal.core.util.IntSerializer
|
||||
|
||||
/**
|
||||
* Attachments/media can come from and go to multiple CDN locations depending on when and where
|
||||
* they were uploaded. This class represents the CDNs where attachments/media can live.
|
||||
*/
|
||||
enum class Cdn(private val value: Int) {
|
||||
S3(-1),
|
||||
CDN_0(0),
|
||||
CDN_2(2),
|
||||
CDN_3(3);
|
||||
|
||||
val cdnNumber: Int
|
||||
get() {
|
||||
return when (this) {
|
||||
S3 -> -1
|
||||
CDN_0 -> 0
|
||||
CDN_2 -> 2
|
||||
CDN_3 -> 3
|
||||
}
|
||||
}
|
||||
|
||||
fun serialize(): Int {
|
||||
return Serializer.serialize(this)
|
||||
}
|
||||
|
||||
companion object Serializer : IntSerializer<Cdn> {
|
||||
override fun serialize(data: Cdn): Int {
|
||||
return data.value
|
||||
}
|
||||
|
||||
override fun deserialize(data: Int): Cdn {
|
||||
return values().first { it.value == data }
|
||||
}
|
||||
|
||||
fun fromCdnNumber(cdnNumber: Int): Cdn {
|
||||
return when (cdnNumber) {
|
||||
-1 -> S3
|
||||
0 -> CDN_0
|
||||
2 -> CDN_2
|
||||
3 -> CDN_3
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -25,6 +26,22 @@ class DatabaseAttachment : Attachment {
|
||||
@JvmField
|
||||
val dataHash: String?
|
||||
|
||||
@JvmField
|
||||
val archiveCdn: Int
|
||||
|
||||
@JvmField
|
||||
val archiveThumbnailCdn: Int
|
||||
|
||||
@JvmField
|
||||
val archiveMediaName: String?
|
||||
|
||||
@JvmField
|
||||
val archiveMediaId: String?
|
||||
|
||||
@JvmField
|
||||
val thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState
|
||||
|
||||
private val hasArchiveThumbnail: Boolean
|
||||
private val hasThumbnail: Boolean
|
||||
val displayOrder: Int
|
||||
|
||||
@@ -33,11 +50,12 @@ class DatabaseAttachment : Attachment {
|
||||
mmsId: Long,
|
||||
hasData: Boolean,
|
||||
hasThumbnail: Boolean,
|
||||
hasArchiveThumbnail: Boolean,
|
||||
contentType: String?,
|
||||
transferProgress: Int,
|
||||
size: Long,
|
||||
fileName: String?,
|
||||
cdnNumber: Int,
|
||||
cdn: Cdn,
|
||||
location: String?,
|
||||
key: String?,
|
||||
digest: ByteArray?,
|
||||
@@ -57,13 +75,19 @@ class DatabaseAttachment : Attachment {
|
||||
transformProperties: TransformProperties?,
|
||||
displayOrder: Int,
|
||||
uploadTimestamp: Long,
|
||||
dataHash: String?
|
||||
dataHash: String?,
|
||||
archiveCdn: Int,
|
||||
archiveThumbnailCdn: Int,
|
||||
archiveMediaName: String?,
|
||||
archiveMediaId: String?,
|
||||
thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState,
|
||||
uuid: UUID?
|
||||
) : super(
|
||||
contentType = contentType!!,
|
||||
transferState = transferProgress,
|
||||
size = size,
|
||||
fileName = fileName,
|
||||
cdnNumber = cdnNumber,
|
||||
cdn = cdn,
|
||||
remoteLocation = location,
|
||||
remoteKey = key,
|
||||
remoteDigest = digest,
|
||||
@@ -80,14 +104,21 @@ class DatabaseAttachment : Attachment {
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
audioHash = audioHash,
|
||||
transformProperties = transformProperties
|
||||
transformProperties = transformProperties,
|
||||
uuid = uuid
|
||||
) {
|
||||
this.attachmentId = attachmentId
|
||||
this.mmsId = mmsId
|
||||
this.hasData = hasData
|
||||
this.dataHash = dataHash
|
||||
this.hasThumbnail = hasThumbnail
|
||||
this.hasArchiveThumbnail = hasArchiveThumbnail
|
||||
this.displayOrder = displayOrder
|
||||
this.archiveCdn = archiveCdn
|
||||
this.archiveThumbnailCdn = archiveThumbnailCdn
|
||||
this.archiveMediaName = archiveMediaName
|
||||
this.archiveMediaId = archiveMediaId
|
||||
this.thumbnailRestoreState = thumbnailRestoreState
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel) {
|
||||
@@ -97,6 +128,12 @@ class DatabaseAttachment : Attachment {
|
||||
hasThumbnail = ParcelUtil.readBoolean(parcel)
|
||||
mmsId = parcel.readLong()
|
||||
displayOrder = parcel.readInt()
|
||||
archiveCdn = parcel.readInt()
|
||||
archiveThumbnailCdn = parcel.readInt()
|
||||
archiveMediaName = parcel.readString()
|
||||
archiveMediaId = parcel.readString()
|
||||
hasArchiveThumbnail = ParcelUtil.readBoolean(parcel)
|
||||
thumbnailRestoreState = AttachmentTable.ThumbnailRestoreState.deserialize(parcel.readInt())
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
@@ -107,10 +144,16 @@ class DatabaseAttachment : Attachment {
|
||||
ParcelUtil.writeBoolean(dest, hasThumbnail)
|
||||
dest.writeLong(mmsId)
|
||||
dest.writeInt(displayOrder)
|
||||
dest.writeInt(archiveCdn)
|
||||
dest.writeInt(archiveThumbnailCdn)
|
||||
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
|
||||
@@ -123,9 +166,16 @@ class DatabaseAttachment : Attachment {
|
||||
null
|
||||
}
|
||||
|
||||
override val thumbnailUri: Uri?
|
||||
get() = if (hasArchiveThumbnail) {
|
||||
PartAuthority.getAttachmentThumbnailUri(attachmentId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -9,10 +9,10 @@ import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.whispersystems.signalservice.api.InvalidMessageStructureException
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
|
||||
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
|
||||
@@ -21,7 +21,7 @@ class PointerAttachment : Attachment {
|
||||
transferState: Int,
|
||||
size: Long,
|
||||
fileName: String?,
|
||||
cdnNumber: Int,
|
||||
cdn: Cdn,
|
||||
location: String,
|
||||
key: String?,
|
||||
digest: ByteArray?,
|
||||
@@ -36,13 +36,14 @@ class PointerAttachment : Attachment {
|
||||
uploadTimestamp: Long,
|
||||
caption: String?,
|
||||
stickerLocator: StickerLocator?,
|
||||
blurHash: BlurHash?
|
||||
blurHash: BlurHash?,
|
||||
uuid: UUID?
|
||||
) : super(
|
||||
contentType = contentType,
|
||||
transferState = transferState,
|
||||
size = size,
|
||||
fileName = fileName,
|
||||
cdnNumber = cdnNumber,
|
||||
cdn = cdn,
|
||||
remoteLocation = location,
|
||||
remoteKey = key,
|
||||
remoteDigest = digest,
|
||||
@@ -60,13 +61,15 @@ class PointerAttachment : Attachment {
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
audioHash = null,
|
||||
transformProperties = null
|
||||
transformProperties = null,
|
||||
uuid = uuid
|
||||
)
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel)
|
||||
|
||||
override val uri: Uri? = null
|
||||
override val publicUri: Uri? = null
|
||||
override val thumbnailUri: Uri? = null
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@@ -83,7 +86,7 @@ class PointerAttachment : Attachment {
|
||||
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
fun forPointer(pointer: Optional<SignalServiceAttachment>, stickerLocator: StickerLocator? = null, fastPreflightId: String? = null): Optional<Attachment> {
|
||||
fun forPointer(pointer: Optional<SignalServiceAttachment>, stickerLocator: StickerLocator? = null, fastPreflightId: String? = null, transferState: Int = AttachmentTable.TRANSFER_PROGRESS_PENDING): Optional<Attachment> {
|
||||
if (!pointer.isPresent || !pointer.get().isPointer) {
|
||||
return Optional.empty()
|
||||
}
|
||||
@@ -97,10 +100,10 @@ class PointerAttachment : Attachment {
|
||||
return Optional.of(
|
||||
PointerAttachment(
|
||||
contentType = pointer.get().contentType,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
transferState = transferState,
|
||||
size = pointer.get().asPointer().size.orElse(0).toLong(),
|
||||
fileName = pointer.get().asPointer().fileName.orElse(null),
|
||||
cdnNumber = pointer.get().asPointer().cdnNumber,
|
||||
cdn = Cdn.fromCdnNumber(pointer.get().asPointer().cdnNumber),
|
||||
location = pointer.get().asPointer().remoteId.toString(),
|
||||
key = encodedKey,
|
||||
digest = pointer.get().asPointer().digest.orElse(null),
|
||||
@@ -115,36 +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))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun forPointer(pointer: SignalServiceDataMessage.Quote.QuotedAttachment): Optional<Attachment> {
|
||||
val thumbnail = pointer.thumbnail
|
||||
|
||||
return Optional.of(
|
||||
PointerAttachment(
|
||||
contentType = pointer.contentType,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(),
|
||||
fileName = pointer.fileName,
|
||||
cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0,
|
||||
location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0",
|
||||
key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null,
|
||||
digest = thumbnail?.asPointer()?.digest?.orElse(null),
|
||||
incrementalDigest = thumbnail?.asPointer()?.incrementalDigest?.orElse(null),
|
||||
incrementalMacChunkSize = thumbnail?.asPointer()?.incrementalMacChunkSize ?: 0,
|
||||
fastPreflightId = null,
|
||||
voiceNote = false,
|
||||
borderless = false,
|
||||
videoGif = false,
|
||||
width = thumbnail?.asPointer()?.width ?: 0,
|
||||
height = thumbnail?.asPointer()?.height ?: 0,
|
||||
uploadTimestamp = thumbnail?.asPointer()?.uploadTimestamp ?: 0,
|
||||
caption = thumbnail?.asPointer()?.caption?.orElse(null),
|
||||
stickerLocator = null,
|
||||
blurHash = null
|
||||
blurHash = BlurHash.parseOrNull(pointer.get().asPointer().blurHash.orElse(null)),
|
||||
uuid = pointer.get().asPointer().uuid
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -166,7 +141,7 @@ class PointerAttachment : Attachment {
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
|
||||
size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(),
|
||||
fileName = quotedAttachment.fileName,
|
||||
cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0,
|
||||
cdn = Cdn.fromCdnNumber(thumbnail?.asPointer()?.cdnNumber ?: 0),
|
||||
location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0",
|
||||
key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null,
|
||||
digest = thumbnail?.asPointer()?.digest?.orElse(null),
|
||||
@@ -181,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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.attachments
|
||||
|
||||
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
|
||||
@@ -17,7 +19,7 @@ class TombstoneAttachment : Attachment {
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
size = 0,
|
||||
fileName = null,
|
||||
cdnNumber = 0,
|
||||
cdn = Cdn.CDN_0,
|
||||
remoteLocation = null,
|
||||
remoteKey = null,
|
||||
remoteDigest = null,
|
||||
@@ -34,11 +36,53 @@ class TombstoneAttachment : Attachment {
|
||||
stickerLocator = null,
|
||||
blurHash = null,
|
||||
audioHash = null,
|
||||
transformProperties = null
|
||||
transformProperties = null,
|
||||
uuid = null
|
||||
)
|
||||
|
||||
constructor(
|
||||
contentType: String?,
|
||||
incrementalMac: ByteArray?,
|
||||
incrementalMacChunkSize: Int?,
|
||||
width: Int?,
|
||||
height: Int?,
|
||||
caption: String?,
|
||||
blurHash: String?,
|
||||
voiceNote: Boolean = false,
|
||||
borderless: Boolean = false,
|
||||
gif: Boolean = false,
|
||||
quote: Boolean,
|
||||
uuid: UUID?
|
||||
) : super(
|
||||
contentType = contentType ?: "",
|
||||
quote = quote,
|
||||
transferState = AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE,
|
||||
size = 0,
|
||||
fileName = null,
|
||||
cdn = Cdn.CDN_0,
|
||||
remoteLocation = null,
|
||||
remoteKey = null,
|
||||
remoteDigest = null,
|
||||
incrementalDigest = incrementalMac,
|
||||
fastPreflightId = null,
|
||||
voiceNote = voiceNote,
|
||||
borderless = borderless,
|
||||
videoGif = gif,
|
||||
width = width ?: 0,
|
||||
height = height ?: 0,
|
||||
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
|
||||
uploadTimestamp = 0,
|
||||
caption = caption,
|
||||
stickerLocator = null,
|
||||
blurHash = BlurHash.parseOrNull(blurHash),
|
||||
audioHash = null,
|
||||
transformProperties = null,
|
||||
uuid = uuid
|
||||
)
|
||||
|
||||
constructor(parcel: Parcel) : super(parcel)
|
||||
|
||||
override val uri: Uri? = null
|
||||
override val publicUri: Uri? = null
|
||||
override val thumbnailUri: Uri? = null
|
||||
}
|
||||
|
||||
@@ -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,13 +65,14 @@ class UriAttachment : Attachment {
|
||||
stickerLocator: StickerLocator?,
|
||||
blurHash: BlurHash?,
|
||||
audioHash: AudioHash?,
|
||||
transformProperties: TransformProperties?
|
||||
transformProperties: TransformProperties?,
|
||||
uuid: UUID? = UUID.randomUUID()
|
||||
) : super(
|
||||
contentType = contentType,
|
||||
transferState = transferState,
|
||||
size = size,
|
||||
fileName = fileName,
|
||||
cdnNumber = 0,
|
||||
cdn = Cdn.CDN_0,
|
||||
remoteLocation = null,
|
||||
remoteKey = null,
|
||||
remoteDigest = null,
|
||||
@@ -87,7 +90,8 @@ class UriAttachment : Attachment {
|
||||
stickerLocator = stickerLocator,
|
||||
blurHash = blurHash,
|
||||
audioHash = audioHash,
|
||||
transformProperties = transformProperties
|
||||
transformProperties = transformProperties,
|
||||
uuid = uuid
|
||||
) {
|
||||
uri = Objects.requireNonNull(dataUri)
|
||||
}
|
||||
@@ -98,6 +102,7 @@ class UriAttachment : Attachment {
|
||||
|
||||
override val uri: Uri
|
||||
override val publicUri: Uri? = null
|
||||
override val thumbnailUri: Uri? = null
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
super.writeToParcel(dest, flags)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
|
||||
import org.thoughtcrime.securesms.groups.ParcelableGroupId
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.permissions.PermissionCompat
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||
import org.thoughtcrime.securesms.permissions.Permissions
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
@@ -222,34 +222,28 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun openCameraCapture() {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.onAllGranted {
|
||||
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
||||
}
|
||||
.onAnyDenied {
|
||||
Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
.execute()
|
||||
if (CameraXUtil.isSupported()) {
|
||||
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
||||
} else {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.CAMERA)
|
||||
.ifNecessary()
|
||||
.onAllGranted {
|
||||
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
|
||||
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
|
||||
}
|
||||
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_allow_camera), R.drawable.symbol_camera_24)
|
||||
.withPermanentDenialDialog(getString(R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos, getParentFragmentManager())
|
||||
.onAnyDenied { Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT).show() }
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyTable;
|
||||
import org.thoughtcrime.securesms.database.PendingRetryReceiptTable;
|
||||
import org.thoughtcrime.securesms.database.ReactionTable;
|
||||
import org.thoughtcrime.securesms.database.RemappedRecordTables;
|
||||
import org.thoughtcrime.securesms.database.SearchTable;
|
||||
import org.thoughtcrime.securesms.database.SenderKeyTable;
|
||||
import org.thoughtcrime.securesms.database.SenderKeySharedTable;
|
||||
@@ -44,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;
|
||||
@@ -92,7 +93,9 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
SenderKeyTable.TABLE_NAME,
|
||||
SenderKeySharedTable.TABLE_NAME,
|
||||
PendingRetryReceiptTable.TABLE_NAME,
|
||||
AvatarPickerDatabase.TABLE_NAME
|
||||
AvatarPickerDatabase.TABLE_NAME,
|
||||
RemappedRecordTables.Recipients.TABLE_NAME,
|
||||
RemappedRecordTables.Threads.TABLE_NAME
|
||||
);
|
||||
|
||||
public static BackupEvent export(@NonNull Context context,
|
||||
@@ -232,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)) {
|
||||
@@ -533,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,7 +31,7 @@ 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;
|
||||
@@ -98,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();
|
||||
@@ -287,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")
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
|
||||
enum class RestoreState(val id: Int, val inProgress: Boolean) {
|
||||
FAILED(-1, false),
|
||||
NONE(0, false),
|
||||
PENDING(1, true),
|
||||
RESTORING_DB(2, true),
|
||||
RESTORING_MEDIA(3, true);
|
||||
|
||||
companion object {
|
||||
val serializer: LongSerializer<RestoreState> = Serializer()
|
||||
}
|
||||
|
||||
class Serializer : LongSerializer<RestoreState> {
|
||||
override fun serialize(data: RestoreState): Long {
|
||||
return data.id.toLong()
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): RestoreState {
|
||||
return when (data.toInt()) {
|
||||
FAILED.id -> FAILED
|
||||
PENDING.id -> PENDING
|
||||
RESTORING_DB.id -> RESTORING_DB
|
||||
RESTORING_MEDIA.id -> RESTORING_MEDIA
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.signal.core.util.LongSerializer
|
||||
|
||||
/**
|
||||
* Describes how often a users messages are backed up.
|
||||
*/
|
||||
enum class BackupFrequency(val id: Int) {
|
||||
DAILY(0),
|
||||
WEEKLY(1),
|
||||
MONTHLY(2),
|
||||
MANUAL(-1);
|
||||
|
||||
companion object Serializer : LongSerializer<BackupFrequency> {
|
||||
override fun serialize(data: BackupFrequency): Long {
|
||||
return data.id.toLong()
|
||||
}
|
||||
|
||||
override fun deserialize(data: Long): BackupFrequency {
|
||||
return when (data.toInt()) {
|
||||
MANUAL.id -> MANUAL
|
||||
DAILY.id -> DAILY
|
||||
WEEKLY.id -> WEEKLY
|
||||
MONTHLY.id -> MONTHLY
|
||||
else -> MANUAL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user