mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-11 12:40:57 +01:00
Compare commits
3059 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
676be03ffc | ||
|
|
527096cc0e | ||
|
|
83c3cc6a6d | ||
|
|
0c4725dfa7 | ||
|
|
2c7668253e | ||
|
|
ab7bdc3c03 | ||
|
|
bb1b548c27 | ||
|
|
216073f4c2 | ||
|
|
84ae8db549 | ||
|
|
09bd460875 | ||
|
|
97ea5dc45e | ||
|
|
85449802d1 | ||
|
|
d683b8a321 | ||
|
|
2b1bbdda15 | ||
|
|
011a36c8f3 | ||
|
|
dd1976d431 | ||
|
|
643f64e181 | ||
|
|
659e36673b | ||
|
|
907918d3fa | ||
|
|
243c86cec3 | ||
|
|
dca10634e6 | ||
|
|
5dfc4c422e | ||
|
|
46753fc617 | ||
|
|
e263d7da73 | ||
|
|
c4ba579310 | ||
|
|
d6d9e5ca64 | ||
|
|
90a8d90e40 | ||
|
|
b61ca37523 | ||
|
|
b7af1e09e2 | ||
|
|
ff47f784a3 | ||
|
|
1f196f74ff | ||
|
|
4152294b57 | ||
|
|
1aaa833127 | ||
|
|
2cfd19add6 | ||
|
|
8e3000d852 | ||
|
|
4e48a445bf | ||
|
|
45833ef24a | ||
|
|
4354a9ff5e | ||
|
|
f1bf6105ea | ||
|
|
282ec6918b | ||
|
|
69d62d385e | ||
|
|
0f7f866562 | ||
|
|
5f66e2eb15 | ||
|
|
3f71f90234 | ||
|
|
204fcc28c7 | ||
|
|
f53cb19943 | ||
|
|
cea8546ce5 | ||
|
|
bb7ee5915c | ||
|
|
cf8e05fa39 | ||
|
|
01cf0b69e0 | ||
|
|
0aa764586e | ||
|
|
532441db24 | ||
|
|
2bc07e87d8 | ||
|
|
60ad879cac | ||
|
|
e1bc04a811 | ||
|
|
23b53ef0f8 | ||
|
|
047ec137c9 | ||
|
|
72cb1528ad | ||
|
|
4b78b44b29 | ||
|
|
e0763fbf86 | ||
|
|
1758a20174 | ||
|
|
c3c7bb7fba | ||
|
|
3818eb6937 | ||
|
|
d15bb05ae3 | ||
|
|
244a81ef24 | ||
|
|
4447433ffe | ||
|
|
46bc2589b5 | ||
|
|
076df8c429 | ||
|
|
8727f0d90d | ||
|
|
8fc21876fe | ||
|
|
a3c476f2ab | ||
|
|
a76d400bd1 | ||
|
|
9b23264502 | ||
|
|
1e58f8097a | ||
|
|
9a24455085 | ||
|
|
4002dea05d | ||
|
|
fd31bc60b2 | ||
|
|
478e3a7233 | ||
|
|
26e79db057 | ||
|
|
71b5a9f865 | ||
|
|
8a4d9fc635 | ||
|
|
d3a6d31873 | ||
|
|
57f36e7b41 | ||
|
|
43491daff9 | ||
|
|
4c9b5926b9 | ||
|
|
cda029cd93 | ||
|
|
82443af8f7 | ||
|
|
1f8481d287 | ||
|
|
b7e9446cde | ||
|
|
cc615fbf87 | ||
|
|
112473bc5c | ||
|
|
8d38f6f5e7 | ||
|
|
66278a0eac | ||
|
|
1fdb3ffb03 | ||
|
|
560086a1c2 | ||
|
|
630875dae2 | ||
|
|
627b939326 | ||
|
|
5c9b7ce7d5 | ||
|
|
5171986aca | ||
|
|
75c84c452b | ||
|
|
110e2c9eb0 | ||
|
|
330cef2702 | ||
|
|
8eb0b2f960 | ||
|
|
c39a1ebdb6 | ||
|
|
69e8c9351d | ||
|
|
7f71d08e11 | ||
|
|
6f2cc923c2 | ||
|
|
5f40144ae9 | ||
|
|
7936552d53 | ||
|
|
5ffb7b07da | ||
|
|
e2e6a73e8d | ||
|
|
f296fcd716 | ||
|
|
fa7e4c9686 | ||
|
|
22dd1da985 | ||
|
|
32a00b5c75 | ||
|
|
263690d3e2 | ||
|
|
54c07dd966 | ||
|
|
e036c8992f | ||
|
|
879c794324 | ||
|
|
57af49953a | ||
|
|
6f6665e1d4 | ||
|
|
d20b610768 | ||
|
|
bcbe39c85c | ||
|
|
b2e1f41b0e | ||
|
|
7cb2c3415a | ||
|
|
d0a06ab3de | ||
|
|
ae923c9221 | ||
|
|
1a5ce9b4b9 | ||
|
|
748cd00883 | ||
|
|
1e4e6b6b41 | ||
|
|
19f3219224 | ||
|
|
6358589e19 | ||
|
|
08d8564c00 | ||
|
|
68c7ce5823 | ||
|
|
bfcbfed0a6 | ||
|
|
8f99930168 | ||
|
|
21019d1726 | ||
|
|
7807d92825 | ||
|
|
4af6e0480a | ||
|
|
1d6917476e | ||
|
|
3bdbd69a7d | ||
|
|
c880db0f4a | ||
|
|
385ba3590c | ||
|
|
c63beb5b2c | ||
|
|
7dc15f8bd3 | ||
|
|
e83c6dc7c2 | ||
|
|
d45acd0e24 | ||
|
|
16a732171a | ||
|
|
b9da045f79 | ||
|
|
ccabd9edd8 | ||
|
|
11d165a17b | ||
|
|
ef2c67d808 | ||
|
|
6ac510a156 | ||
|
|
da74874815 | ||
|
|
ff9c77c2e2 | ||
|
|
2c11a27897 | ||
|
|
8dc910e71d | ||
|
|
cbd587f142 | ||
|
|
ffd2e164bf | ||
|
|
ddb367edbe | ||
|
|
981e7a4270 | ||
|
|
b46b04fcdf | ||
|
|
aebaff736c | ||
|
|
8932eef991 | ||
|
|
bb01c0501b | ||
|
|
1d1ea01cc1 | ||
|
|
2677665069 | ||
|
|
ea215ef488 | ||
|
|
d9586e8d00 | ||
|
|
e1283a66fd | ||
|
|
fb82703740 | ||
|
|
5e03e31ffd | ||
|
|
4091af3632 | ||
|
|
1b729c42b6 | ||
|
|
5c139aa5b1 | ||
|
|
dc7208922c | ||
|
|
6424c6bc99 | ||
|
|
57adab858c | ||
|
|
4f001a0c95 | ||
|
|
12529e9fb0 | ||
|
|
d406e8f5b8 | ||
|
|
fb2a332513 | ||
|
|
c2bdac80dc | ||
|
|
db00000d66 | ||
|
|
f551a94e58 | ||
|
|
58a7d3dc08 | ||
|
|
3380fa722d | ||
|
|
216f57f3ea | ||
|
|
bf4aa85ac3 | ||
|
|
e4966da3ef | ||
|
|
79c7c2345f | ||
|
|
d516037be9 | ||
|
|
008b38594d | ||
|
|
15b59457f7 | ||
|
|
5ddd1651ee | ||
|
|
8ca89d2024 | ||
|
|
585c8cd863 | ||
|
|
faf6ab233f | ||
|
|
95cbc91bf0 | ||
|
|
9480e23455 | ||
|
|
727a0f8273 | ||
|
|
279e55d65f | ||
|
|
c36fba2ab7 | ||
|
|
1d0997379f | ||
|
|
1a7611d505 | ||
|
|
36846301de | ||
|
|
b8e81e6677 | ||
|
|
3d169bffd0 | ||
|
|
556a25447e | ||
|
|
b42e48a08a | ||
|
|
b1a4e889bc | ||
|
|
e6fb01a67b | ||
|
|
6c042f6e47 | ||
|
|
f87ff58701 | ||
|
|
4fb335de28 | ||
|
|
725d8dc85d | ||
|
|
e76153b2fd | ||
|
|
0b98901634 | ||
|
|
57feb272d2 | ||
|
|
7b0badef19 | ||
|
|
36640edfee | ||
|
|
4e07c07ca9 | ||
|
|
31ddc5bcc0 | ||
|
|
c80f459c37 | ||
|
|
2a6dab41f5 | ||
|
|
e6d8e36141 | ||
|
|
816c9360cd | ||
|
|
82c3265be5 | ||
|
|
ab03a627da | ||
|
|
81a45ddc09 | ||
|
|
f1115130b2 | ||
|
|
2d557215a0 | ||
|
|
cc806a2f79 | ||
|
|
853c934a5a | ||
|
|
f1ba947a59 | ||
|
|
44b2c62a0e | ||
|
|
06d475fb6e | ||
|
|
36dface175 | ||
|
|
86cf8200b5 | ||
|
|
973dc72cfa | ||
|
|
eb59afc33c | ||
|
|
625ca832b0 | ||
|
|
bc6face873 | ||
|
|
0aeaec8b67 | ||
|
|
b70b058925 | ||
|
|
e17cf37799 | ||
|
|
330debcf37 | ||
|
|
6641cc4806 | ||
|
|
bd3ab2cc38 | ||
|
|
fa487e1885 | ||
|
|
eb2fc33940 | ||
|
|
c39739bcb4 | ||
|
|
e7720640d1 | ||
|
|
6e55bc04ab | ||
|
|
56ab95b0e1 | ||
|
|
678c1459e9 | ||
|
|
e87b73cc19 | ||
|
|
45e1ecd07e | ||
|
|
7b043d4143 | ||
|
|
046d439887 | ||
|
|
b34bf4b8b0 | ||
|
|
fddc99ab4f | ||
|
|
1007111310 | ||
|
|
3346a1e918 | ||
|
|
9c391eb2c9 | ||
|
|
f714e038a0 | ||
|
|
61405a62c2 | ||
|
|
d424a60345 | ||
|
|
3c10966a36 | ||
|
|
e210d5939c | ||
|
|
1fafcc69ff | ||
|
|
3184368fa7 | ||
|
|
c622b7fdb1 | ||
|
|
29ead80e17 | ||
|
|
9a72833e06 | ||
|
|
d0baf1dc95 | ||
|
|
7f1227ee19 | ||
|
|
34c95dc082 | ||
|
|
10ad73f201 | ||
|
|
21fab7c5ba | ||
|
|
e9c2f96bb9 | ||
|
|
a950462451 | ||
|
|
809317c0fd | ||
|
|
6634540183 | ||
|
|
89bfba3ee9 | ||
|
|
97974291d2 | ||
|
|
6daee5719b | ||
|
|
58443c46be | ||
|
|
8cbecc2992 | ||
|
|
4c0ca48af3 | ||
|
|
91eeda6c6e | ||
|
|
04e75c18dd | ||
|
|
f5fbfbc7fd | ||
|
|
0e1df94b54 | ||
|
|
dd923629f6 | ||
|
|
df8992aaca | ||
|
|
63b9700865 | ||
|
|
d309877d63 | ||
|
|
f247fd78c6 | ||
|
|
4f96cb7439 | ||
|
|
6e6e3a5eba | ||
|
|
9166ed64fb | ||
|
|
25e4eaa8e8 | ||
|
|
6e742ce770 | ||
|
|
c134c3033e | ||
|
|
fb43a8257c | ||
|
|
cecfe80d61 | ||
|
|
6c302b708a | ||
|
|
6a22919c50 | ||
|
|
9073ce5c7b | ||
|
|
9024c19169 | ||
|
|
60a0565ba8 | ||
|
|
383f7556e3 | ||
|
|
94795599e2 | ||
|
|
84fbb7c466 | ||
|
|
c339f39b70 | ||
|
|
41a3609f06 | ||
|
|
f5abd7acdf | ||
|
|
414368e251 | ||
|
|
a3d1197aef | ||
|
|
d91760eefc | ||
|
|
ee20ced238 | ||
|
|
066892c11a | ||
|
|
69fd4f79db | ||
|
|
f49e2768c1 | ||
|
|
70378b85d7 | ||
|
|
585401a98e | ||
|
|
cf7ebfa03d | ||
|
|
aec0a9951a | ||
|
|
b113eec940 | ||
|
|
a966812bfc | ||
|
|
3879a8ffdb | ||
|
|
5b949b0116 | ||
|
|
3c13619ce8 | ||
|
|
24bba98122 | ||
|
|
a96e5e6ae6 | ||
|
|
4cfdfab31e | ||
|
|
77d3116431 | ||
|
|
b943df1ce4 | ||
|
|
8bbb7d56e0 | ||
|
|
079a3d4fee | ||
|
|
176e0e7765 | ||
|
|
c73e80f8d9 | ||
|
|
47cd1b568f | ||
|
|
058c523329 | ||
|
|
84515482a6 | ||
|
|
02629020df | ||
|
|
58d769b21f | ||
|
|
9dc67e0466 | ||
|
|
72d02104dc | ||
|
|
371a39049d | ||
|
|
47e4a6cf5a | ||
|
|
4a41e9f9a1 | ||
|
|
9fa1b58019 | ||
|
|
c24473e176 | ||
|
|
1311ec498f | ||
|
|
251cec5dee | ||
|
|
1e15a8c1d3 | ||
|
|
9c5c58794b | ||
|
|
50063854d7 | ||
|
|
79d2041e46 | ||
|
|
479b27ce94 | ||
|
|
a66857a7cc | ||
|
|
37815a3f39 | ||
|
|
b55ba67b66 | ||
|
|
37a2d5fbca | ||
|
|
d7b5c6bff3 | ||
|
|
f11028529e | ||
|
|
93ec322bb9 | ||
|
|
71e0468d2c | ||
|
|
816e3442a0 | ||
|
|
c37ed722dc | ||
|
|
e08c2966c3 | ||
|
|
976f80ff7e | ||
|
|
34a4bda331 | ||
|
|
a4077ccb4a | ||
|
|
21ada2a503 | ||
|
|
57a70c3085 | ||
|
|
16c8b88f0f | ||
|
|
b806952430 | ||
|
|
c0da0bd272 | ||
|
|
45239c2264 | ||
|
|
690236c4e5 | ||
|
|
ebee3f72e6 | ||
|
|
37cec7d44f | ||
|
|
187fd63a75 | ||
|
|
362cdfc463 | ||
|
|
863b443317 | ||
|
|
341c474610 | ||
|
|
cbb3c0911c | ||
|
|
890facc6f6 | ||
|
|
6fa8337058 | ||
|
|
3f1cb65e02 | ||
|
|
3551e7ec00 | ||
|
|
5ecf60a306 | ||
|
|
6659700a1c | ||
|
|
070174fee6 | ||
|
|
09003d85b1 | ||
|
|
ea87108def | ||
|
|
7a696f9a62 | ||
|
|
8ba57a2733 | ||
|
|
9824cc2cbe | ||
|
|
ad60cc72cb | ||
|
|
1950b80402 | ||
|
|
2acb47952b | ||
|
|
14cacaef86 | ||
|
|
958e815933 | ||
|
|
6b50be78c0 | ||
|
|
680223c4b6 | ||
|
|
1af914d5ef | ||
|
|
a2fc710261 | ||
|
|
10922594b3 | ||
|
|
abd80c5204 | ||
|
|
ff589e3b91 | ||
|
|
c80ccd70ec | ||
|
|
d22d18da47 | ||
|
|
75b41c34ea | ||
|
|
11557e4815 | ||
|
|
d698f74d0b | ||
|
|
ecbea9fd95 | ||
|
|
13f7a64139 | ||
|
|
39cb1c638e | ||
|
|
489b58ad67 | ||
|
|
f20fe33af9 | ||
|
|
6adddf4a0c | ||
|
|
16773c9b17 | ||
|
|
4b50365fa9 | ||
|
|
98766b9ebb | ||
|
|
45a739ce92 | ||
|
|
c0d7145ada | ||
|
|
f94c007af8 | ||
|
|
df19cb5795 | ||
|
|
e6ceb55092 | ||
|
|
bfe2b5cba9 | ||
|
|
571004df50 | ||
|
|
f32b59f0aa | ||
|
|
e4019d8595 | ||
|
|
0b66a8701e | ||
|
|
e62b8de1bc | ||
|
|
d5cd790871 | ||
|
|
664c22d8f1 | ||
|
|
143a61e312 | ||
|
|
baaad0e475 | ||
|
|
7086709082 | ||
|
|
7bd5ad8c0b | ||
|
|
df19c91ae2 | ||
|
|
e5872037e0 | ||
|
|
b782fabbb6 | ||
|
|
485b466bd2 | ||
|
|
3beac6dfa9 | ||
|
|
98290a9fa3 | ||
|
|
13dd59f226 | ||
|
|
d9c42a4135 | ||
|
|
644b93e5a3 | ||
|
|
3ff218f9c6 | ||
|
|
f572eb5322 | ||
|
|
d3eb480d31 | ||
|
|
ac52b5b992 | ||
|
|
5c181e774f | ||
|
|
05d25718da | ||
|
|
66c50bef44 | ||
|
|
26bd59c378 | ||
|
|
e90eae6080 | ||
|
|
86a7db7653 | ||
|
|
230de7e9dc | ||
|
|
4b8546a151 | ||
|
|
ecd214b91b | ||
|
|
6b5de6e3e5 | ||
|
|
58b6e49aae | ||
|
|
c480512600 | ||
|
|
3a5b6476aa | ||
|
|
cb171092cf | ||
|
|
71979b34db | ||
|
|
73142cea39 | ||
|
|
2ab2c6f039 | ||
|
|
0bea15c0af | ||
|
|
cbd78d78ba | ||
|
|
ac0604a753 | ||
|
|
0e57335be1 | ||
|
|
c4e64f6fa3 | ||
|
|
bf9716f206 | ||
|
|
057ffdbaaf | ||
|
|
65dc0d3f34 | ||
|
|
173ee95e62 | ||
|
|
789339afa7 | ||
|
|
21b518da7a | ||
|
|
57b6b8dcf1 | ||
|
|
543a85316e | ||
|
|
2fedb3a0ee | ||
|
|
ae450aed67 | ||
|
|
0abb4727fc | ||
|
|
4bc6eb96ff | ||
|
|
e6a126d416 | ||
|
|
fdf858f379 | ||
|
|
4151d123cd | ||
|
|
c8a9759eba | ||
|
|
c59b74627f | ||
|
|
f2191d2996 | ||
|
|
7dfffbd50b | ||
|
|
329fc52077 | ||
|
|
8976111f61 | ||
|
|
7402959ac6 | ||
|
|
220d3877a2 | ||
|
|
380c33642c | ||
|
|
7acb2bef3d | ||
|
|
1a103106a5 | ||
|
|
6025e423e8 | ||
|
|
54656ea14e | ||
|
|
fd00ed71b5 | ||
|
|
d4fba5f3c7 | ||
|
|
ce244f2e8f | ||
|
|
4ad466390f | ||
|
|
c5c9b09f7b | ||
|
|
0638b31c1f | ||
|
|
c3c713a75a | ||
|
|
9af1c72233 | ||
|
|
500a1e46ad | ||
|
|
4b446877af | ||
|
|
015548613a | ||
|
|
30b339a482 | ||
|
|
6dcb2e8d24 | ||
|
|
3e8e17526b | ||
|
|
30ecaf7aea | ||
|
|
f761008509 | ||
|
|
c3ab8dddd0 | ||
|
|
164f089d37 | ||
|
|
a021b400bd | ||
|
|
fac8f403be | ||
|
|
d85ab37828 | ||
|
|
1e35403c87 | ||
|
|
b99c2165fa | ||
|
|
303100bb6b | ||
|
|
b71ba79b8a | ||
|
|
54cd84b842 | ||
|
|
1565ecdcea | ||
|
|
0a99b68d25 | ||
|
|
f4fac5bd90 | ||
|
|
f6760b90da | ||
|
|
ad9b1f05b4 | ||
|
|
17581a7a5e | ||
|
|
b41bf66133 | ||
|
|
8bb3d71472 | ||
|
|
295d4b9466 | ||
|
|
5e490376f4 | ||
|
|
fa27531c00 | ||
|
|
2737e5613c | ||
|
|
d84612ebf4 | ||
|
|
96165ad5a8 | ||
|
|
19caef057e | ||
|
|
29cafb11eb | ||
|
|
7e458bfde0 | ||
|
|
2a3cb80217 | ||
|
|
3d382ee15e | ||
|
|
6069dfc6f8 | ||
|
|
dee19ed94a | ||
|
|
905b0681f5 | ||
|
|
b6a4e1f145 | ||
|
|
a0131bf39b | ||
|
|
7ed77a00df | ||
|
|
887c173d8f | ||
|
|
6362da7a50 | ||
|
|
1296365bed | ||
|
|
99ae7c5961 | ||
|
|
5c3ea712fe | ||
|
|
bc5cb454bf | ||
|
|
8a7c2c1e20 | ||
|
|
a81a675d59 | ||
|
|
1c66da7873 | ||
|
|
afe3cd1098 | ||
|
|
4f3ee9ca1d | ||
|
|
7771aaa501 | ||
|
|
5ad38c7960 | ||
|
|
0fb1514da2 | ||
|
|
f37efd7e15 | ||
|
|
1ae2464df1 | ||
|
|
0425b70d31 | ||
|
|
7b0d3f36dc | ||
|
|
14b917dc7e | ||
|
|
6184cc0307 | ||
|
|
870aa8e7b0 | ||
|
|
d88016669b | ||
|
|
a464b413d9 | ||
|
|
d719edf104 | ||
|
|
b36b00a11c | ||
|
|
a99db2b16e | ||
|
|
2744dec43a | ||
|
|
6f2cce1494 | ||
|
|
689ee243aa | ||
|
|
537fc0ef5c | ||
|
|
e647b31f29 | ||
|
|
b59932cd88 | ||
|
|
cfb4377de3 | ||
|
|
e861c022da | ||
|
|
59006d3182 | ||
|
|
503faea3a9 | ||
|
|
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 | ||
|
|
ebbf8fad4b | ||
|
|
5891c6fb2d | ||
|
|
7c96319fb6 | ||
|
|
0d652ccfd6 | ||
|
|
d3718aa7ef | ||
|
|
fcdcb9fd33 | ||
|
|
a8f925def0 | ||
|
|
53cb125712 | ||
|
|
2a5793d96e | ||
|
|
d460fa7ed4 | ||
|
|
5272b13c41 | ||
|
|
a66ac42038 | ||
|
|
0014a2cba7 | ||
|
|
7a9c01e6e5 | ||
|
|
16402e43a5 | ||
|
|
b1944da58d | ||
|
|
9081d3c826 | ||
|
|
d4ae0ca4cb | ||
|
|
88f6ab915e | ||
|
|
939024faff | ||
|
|
4c6e7991df | ||
|
|
036d91c039 | ||
|
|
869c922532 | ||
|
|
217d15a853 | ||
|
|
931ffd0ba3 | ||
|
|
fecac297fa | ||
|
|
b0ea8d7df5 | ||
|
|
f126df2120 | ||
|
|
42450024fc | ||
|
|
101db6e164 | ||
|
|
13bef94bf7 | ||
|
|
02792c5a6f | ||
|
|
303929090b | ||
|
|
7a24554b68 | ||
|
|
5b10aa6fa7 | ||
|
|
e6eefac609 | ||
|
|
5f5a80dcbe | ||
|
|
7802448b24 | ||
|
|
16d231f718 | ||
|
|
62ca6cdd2f | ||
|
|
7d81ed1150 | ||
|
|
27812bb1ec | ||
|
|
6854f7eb2a | ||
|
|
de86c5622d | ||
|
|
6bf1a4295f | ||
|
|
7de2f0f460 | ||
|
|
50149a3803 | ||
|
|
d7ee9639fd | ||
|
|
7d5627b17b | ||
|
|
e24c951d83 | ||
|
|
e6a11c1ccf | ||
|
|
3f66981359 | ||
|
|
874f808d56 | ||
|
|
450dc2f368 | ||
|
|
7a69df42a7 | ||
|
|
1ce1e30d32 | ||
|
|
011f1d592e | ||
|
|
1d29b0166d | ||
|
|
6df1a68213 | ||
|
|
b7ee6bfcb3 | ||
|
|
c0cb2b5e12 | ||
|
|
b38865bdc7 | ||
|
|
6f46331772 | ||
|
|
989bd662c6 | ||
|
|
359e593481 | ||
|
|
b7e0fe22db | ||
|
|
61cfbd6852 | ||
|
|
02c0d3ed6e | ||
|
|
e4d6c3aeb2 | ||
|
|
0c6761fcfd | ||
|
|
8f884fdd5c | ||
|
|
07cea1818e | ||
|
|
132bc15373 | ||
|
|
d993748753 | ||
|
|
3372565a39 | ||
|
|
134ac2b2fd | ||
|
|
0e0e91b4fe | ||
|
|
25b50bdb8f | ||
|
|
1988085171 | ||
|
|
f892e9baff | ||
|
|
4828d84caf | ||
|
|
aeae6ac292 | ||
|
|
0544c1f249 | ||
|
|
5027159ed8 | ||
|
|
ce778be895 | ||
|
|
9e349d2b30 | ||
|
|
72f19758db | ||
|
|
55bce1fa12 | ||
|
|
5e1ebaa5d4 | ||
|
|
742c348998 | ||
|
|
9d46b52786 | ||
|
|
ef374952ab | ||
|
|
f8ef4d5985 | ||
|
|
85929809f0 | ||
|
|
068540120e | ||
|
|
471c4fc200 | ||
|
|
398c67362d | ||
|
|
4ceeda5f02 | ||
|
|
2bf6b993fe | ||
|
|
68363c5b82 | ||
|
|
9f47a41017 | ||
|
|
ba70101efd | ||
|
|
3aa54c9982 | ||
|
|
825ca0d737 | ||
|
|
6754fef164 | ||
|
|
4c079a8c25 | ||
|
|
6e09d101b5 | ||
|
|
39aa583297 | ||
|
|
b08db7a8c5 | ||
|
|
865bf0d056 | ||
|
|
d52c520c02 | ||
|
|
1eabf11cdb | ||
|
|
cfb16d3f17 | ||
|
|
d5707638a6 | ||
|
|
5cda5db7f7 | ||
|
|
5c5d55d265 | ||
|
|
4dd3b92eda | ||
|
|
112579079f | ||
|
|
9897ba4b28 | ||
|
|
c64dfff4c7 | ||
|
|
915b3f0cd3 | ||
|
|
c295d11fc4 | ||
|
|
bc47c5436d | ||
|
|
aaeba4efe1 | ||
|
|
3c0eb58381 | ||
|
|
c4f22449f9 | ||
|
|
bca346ec2f | ||
|
|
e0bd60f87c | ||
|
|
aeedab1531 | ||
|
|
c959f41c68 | ||
|
|
9ba755da16 | ||
|
|
34026c5538 | ||
|
|
ea64425456 | ||
|
|
eb34a20195 | ||
|
|
445513cc32 | ||
|
|
e431518a9d | ||
|
|
61df88e094 | ||
|
|
891c130e12 | ||
|
|
b4ced5278e | ||
|
|
10364e9342 | ||
|
|
74dc222a54 | ||
|
|
2e4ac7ede1 | ||
|
|
184c1b67cc | ||
|
|
f702338129 | ||
|
|
4b4b263423 | ||
|
|
83c16a46de | ||
|
|
6383896a79 | ||
|
|
5fa1560a10 | ||
|
|
9bd6ad36cc | ||
|
|
83cc7d5181 | ||
|
|
44150673e9 | ||
|
|
5092d723a8 | ||
|
|
218964cbda | ||
|
|
ccc9752485 | ||
|
|
619038f27d | ||
|
|
9f197b12ed | ||
|
|
690608cdf3 | ||
|
|
4035932340 | ||
|
|
fc9d94701c | ||
|
|
6d54ae5f3d | ||
|
|
c53abe0941 | ||
|
|
276e253fdf | ||
|
|
f160e960be | ||
|
|
41b57b9207 | ||
|
|
56eae8c7bf | ||
|
|
46c8b3b690 | ||
|
|
58b11f3c47 | ||
|
|
40b4b316b3 | ||
|
|
7a31f69aea | ||
|
|
648c99e81d | ||
|
|
56b482a26f | ||
|
|
c6df4af53a | ||
|
|
32fe927bfc | ||
|
|
5740b768d0 | ||
|
|
d8e74c730a | ||
|
|
58846bbf42 | ||
|
|
78d30fc479 | ||
|
|
86afa988a0 | ||
|
|
6104ef62df | ||
|
|
3f89acf9bd | ||
|
|
591d499462 | ||
|
|
c31a7152bc | ||
|
|
343cc3ca67 | ||
|
|
23e18cee22 | ||
|
|
5b2c458bcf | ||
|
|
c10c64a6a6 | ||
|
|
957221e118 | ||
|
|
e18e4454e4 | ||
|
|
e1067e30de | ||
|
|
09b0f15294 | ||
|
|
b1d6ff4bbd | ||
|
|
a49e9dd96d | ||
|
|
5e428e2c4d | ||
|
|
0f6ff3c101 | ||
|
|
1ade8b502f | ||
|
|
cc25f0685c | ||
|
|
e91ed88785 | ||
|
|
39bc6d5eb3 | ||
|
|
b7f472b0cd | ||
|
|
942f4a45bf | ||
|
|
767896b14c | ||
|
|
8c35628863 | ||
|
|
d555370076 | ||
|
|
bbbe76697d | ||
|
|
fc1d60e65b | ||
|
|
9dc856202a | ||
|
|
6bc41776b1 | ||
|
|
940cee0f30 | ||
|
|
cdb6c16473 | ||
|
|
c4842ae7c5 | ||
|
|
dc32e51ac2 | ||
|
|
43caaf7efc | ||
|
|
dcd0d433b0 | ||
|
|
763e891dfd | ||
|
|
c04f761f5a | ||
|
|
b147882e4f | ||
|
|
c9f5f91aad | ||
|
|
0a3de42729 | ||
|
|
64fc0209f4 | ||
|
|
418ad51e77 | ||
|
|
16faf41a84 | ||
|
|
d5cf8d36b3 | ||
|
|
755fafb0b6 | ||
|
|
8fc9893ecd | ||
|
|
9071fd0024 | ||
|
|
9a3233bb28 | ||
|
|
e77bc9170a | ||
|
|
23d6a71a3b | ||
|
|
67c3f41dff | ||
|
|
e22fa499c2 | ||
|
|
fdef13ae92 | ||
|
|
c0a6f2316c | ||
|
|
7d6a87c825 | ||
|
|
6c863fe99c | ||
|
|
5cf8242ea0 | ||
|
|
a804e8a27c | ||
|
|
3b598e2f07 | ||
|
|
03c5a254e8 | ||
|
|
bdb34e16c6 | ||
|
|
e7c018283a | ||
|
|
ebd8d85a3d | ||
|
|
3f8a9e1be2 | ||
|
|
0d5961baf9 | ||
|
|
872ee805d1 | ||
|
|
b19bcd88b9 | ||
|
|
5c9d65386b | ||
|
|
a86a0938ce | ||
|
|
a886e5f9a0 | ||
|
|
83c1bd61cb | ||
|
|
4ce1789110 | ||
|
|
f484fdbbac | ||
|
|
57ac7cb328 | ||
|
|
47cdc50a81 | ||
|
|
555ddb5b20 | ||
|
|
8e8ba23da7 | ||
|
|
54a1b97167 | ||
|
|
7530d44d28 | ||
|
|
252aa3714e | ||
|
|
ce09e9a217 | ||
|
|
8797236b5a | ||
|
|
6097e6c305 | ||
|
|
879fca0e11 | ||
|
|
c359ddf3c8 | ||
|
|
8ad77ac7aa | ||
|
|
bd3b779282 | ||
|
|
42b805eb91 | ||
|
|
107f2cd3b1 | ||
|
|
c713ccf76c | ||
|
|
dd9c65012b | ||
|
|
31e872a34e | ||
|
|
81579dc9bf | ||
|
|
3c6c03cd75 | ||
|
|
ba41df19bb | ||
|
|
0cc7178cdc | ||
|
|
239e4a7e66 | ||
|
|
0b031d35e3 | ||
|
|
32e81049f5 | ||
|
|
6a65a1c149 | ||
|
|
36ea2a7f5d | ||
|
|
cc40c9d09f | ||
|
|
fb9e731e00 | ||
|
|
264607e0f3 | ||
|
|
cdfc42cc38 | ||
|
|
19cfae1da5 | ||
|
|
577b11a349 | ||
|
|
671dfceac3 | ||
|
|
5626fb74ae | ||
|
|
88d6c91517 | ||
|
|
aa76cefb1c | ||
|
|
a4fde60c1c | ||
|
|
8e86612fc2 | ||
|
|
f6ded23383 | ||
|
|
2ab689c59b | ||
|
|
155f6a88f8 | ||
|
|
3d84fc9c98 | ||
|
|
976e146248 | ||
|
|
d9b0723194 | ||
|
|
0630a6910a | ||
|
|
bc930345b9 | ||
|
|
d7d7963101 | ||
|
|
fa2551dfcf | ||
|
|
b260a47b49 | ||
|
|
47e55fc621 | ||
|
|
700fe5e463 | ||
|
|
ca3d239ce2 | ||
|
|
12385b9c5a | ||
|
|
65b26adb3d | ||
|
|
31b8927291 | ||
|
|
94ffbb3e8e | ||
|
|
1b7616b4db | ||
|
|
71850e1e35 | ||
|
|
380fb60791 | ||
|
|
2cf9fa0524 | ||
|
|
c8c4fdc65e | ||
|
|
9fb16be74f | ||
|
|
2d4d4aab66 | ||
|
|
f18070b78c | ||
|
|
adf1d8a43a | ||
|
|
f360934ddd | ||
|
|
b986c4d54c | ||
|
|
4545e70384 | ||
|
|
3c5cbc3114 | ||
|
|
879b0ad95d | ||
|
|
9982810edb | ||
|
|
699b788187 | ||
|
|
bd61044643 | ||
|
|
6804d58323 | ||
|
|
bfe57b4b8f | ||
|
|
e3fe852a34 | ||
|
|
673fe2625c | ||
|
|
e798feb1d7 | ||
|
|
fa937f9f43 | ||
|
|
ec5f3fc333 | ||
|
|
d85c150a8c | ||
|
|
a5faf0e098 | ||
|
|
1234c63836 | ||
|
|
91920319c7 | ||
|
|
950d9d5a4c | ||
|
|
467dae8132 | ||
|
|
d1ef9d5dcf | ||
|
|
a60419a442 | ||
|
|
f97a034c34 | ||
|
|
4d0fbe2343 | ||
|
|
1d5e108cd4 | ||
|
|
716afc98ac | ||
|
|
459607adae | ||
|
|
784b705265 | ||
|
|
326b95bd10 | ||
|
|
466acaf504 | ||
|
|
838165c3e6 | ||
|
|
e1d7ad7d03 | ||
|
|
3776e86b83 | ||
|
|
ea60858a07 | ||
|
|
d4488c72fb | ||
|
|
9146f2fb30 | ||
|
|
92993f967e | ||
|
|
2c2735af6d | ||
|
|
80c0e19692 | ||
|
|
c9e2162afc | ||
|
|
9a52f4e3ff | ||
|
|
c0235d4cc2 | ||
|
|
f1704fbb57 | ||
|
|
38d5d3ad1b | ||
|
|
ec96b4e3aa | ||
|
|
aa33fd44b8 | ||
|
|
98865d61dd | ||
|
|
0036b8e2d6 | ||
|
|
c021d26103 | ||
|
|
8bca5b4901 | ||
|
|
d4db6c8912 | ||
|
|
c93b4909f4 | ||
|
|
67d3c8e777 | ||
|
|
8d44222097 | ||
|
|
9cb2024334 | ||
|
|
e71bb33b23 | ||
|
|
4ada7c9be9 | ||
|
|
56a2d8891f | ||
|
|
4585e90a00 | ||
|
|
86d2ddc168 | ||
|
|
9d1514308a | ||
|
|
7abff55981 | ||
|
|
f9d7eba7d4 | ||
|
|
9ce021afa2 | ||
|
|
6fc9055221 | ||
|
|
a3438d3345 | ||
|
|
d2cbf11264 | ||
|
|
c584156c86 | ||
|
|
78e04f3ad8 | ||
|
|
6302725678 | ||
|
|
431e65808d | ||
|
|
653914f47e | ||
|
|
96823e944d | ||
|
|
ee19520e1b | ||
|
|
6f16b3fee7 | ||
|
|
89ee7e8e19 | ||
|
|
3c7996aa99 | ||
|
|
3a314c565c | ||
|
|
8c9b668cd7 | ||
|
|
7666462de2 | ||
|
|
54cf11a78b | ||
|
|
16b78f0843 | ||
|
|
5e97a6b192 | ||
|
|
595cced5b7 | ||
|
|
d17f12dd76 | ||
|
|
8b5498cfbd | ||
|
|
e4b755ced8 | ||
|
|
5b7eb9c332 | ||
|
|
cd8e07c102 | ||
|
|
a36f31c2d0 | ||
|
|
ac0812a6dd | ||
|
|
69c864f984 | ||
|
|
3c9a7fd329 | ||
|
|
f81dc11f61 | ||
|
|
ce9a8f62d4 | ||
|
|
7e00d50078 | ||
|
|
4af3f5038f | ||
|
|
7bb1c58452 | ||
|
|
8e7383be05 | ||
|
|
bc5d27ed90 | ||
|
|
ae884d79a1 | ||
|
|
cf89c988cf | ||
|
|
c54313c32e | ||
|
|
3e001ddf1b | ||
|
|
c41795e7f0 | ||
|
|
52120afdbd | ||
|
|
73d98da32b | ||
|
|
99f936ff97 | ||
|
|
15afaeabe3 | ||
|
|
debf964b5f | ||
|
|
393730cea9 | ||
|
|
2194fbd535 | ||
|
|
cf59249d3d | ||
|
|
2c554a3a20 | ||
|
|
7b9554a42c | ||
|
|
dd527ce33c | ||
|
|
ddcc06c6b7 | ||
|
|
a827033f25 | ||
|
|
01841a4aa8 | ||
|
|
bb52e7159c | ||
|
|
3988b46a60 | ||
|
|
caa5e233df | ||
|
|
c7609f9a2a | ||
|
|
750fd4efe1 | ||
|
|
e361795184 | ||
|
|
64fff2adb2 | ||
|
|
846e398577 | ||
|
|
c1e9ee7a66 | ||
|
|
8dc9e09f31 | ||
|
|
d1930d4936 | ||
|
|
14539eb036 | ||
|
|
45f1853c44 | ||
|
|
6eaebd112b | ||
|
|
f0503faeff | ||
|
|
64052d9dd2 | ||
|
|
db4634a0dd | ||
|
|
c725a2fabb | ||
|
|
1ddececa16 | ||
|
|
4e2e6cd83e | ||
|
|
efcfe2dafc | ||
|
|
b8ddb9e673 | ||
|
|
a1f19e9d8a | ||
|
|
5464edf639 | ||
|
|
179c3790e6 | ||
|
|
cfae9753a3 | ||
|
|
61a4a3b322 | ||
|
|
c16bf65a80 | ||
|
|
16ea1912b4 | ||
|
|
54012cb33a | ||
|
|
459c5c0a55 | ||
|
|
4216b56443 | ||
|
|
d7b79314d9 | ||
|
|
a340b13f65 | ||
|
|
72f6b15dba | ||
|
|
64dbb77e63 | ||
|
|
af10b0e4f6 | ||
|
|
6f15c16a42 | ||
|
|
86158027d7 | ||
|
|
50369890f7 | ||
|
|
b8dea25aef | ||
|
|
64e9324aa0 | ||
|
|
20f8c69b07 | ||
|
|
dd1a15c249 | ||
|
|
8b24498fa7 | ||
|
|
3673fa4908 | ||
|
|
960c1df5e7 | ||
|
|
8c3c7c18ad | ||
|
|
b96a5af133 | ||
|
|
d0d4008100 | ||
|
|
17a6fcafa1 | ||
|
|
fb75440769 | ||
|
|
fe39b5e4e2 | ||
|
|
62b142cdeb | ||
|
|
ffce7213b4 | ||
|
|
4205934806 | ||
|
|
7aab86643a | ||
|
|
1bb0c55d88 | ||
|
|
d22ac9ee00 | ||
|
|
80a7db2511 | ||
|
|
e0fb102572 | ||
|
|
8d1a16dcd6 | ||
|
|
0b4bbd5db2 | ||
|
|
78b714e019 | ||
|
|
5022d81d9a | ||
|
|
deacf28d77 | ||
|
|
5e8d324860 | ||
|
|
3554f82ea3 | ||
|
|
888a40a5c4 | ||
|
|
363953a0a4 | ||
|
|
e599d9b14e | ||
|
|
a33be1fad3 | ||
|
|
b6528e843e | ||
|
|
10c31e6591 | ||
|
|
8fba64cb8f | ||
|
|
c2fd08ca80 | ||
|
|
940bf0603e | ||
|
|
4d8a3dafe0 | ||
|
|
d237bb0136 | ||
|
|
d42dfd3edd | ||
|
|
ab4f17d55f | ||
|
|
07968febe8 | ||
|
|
67ff0892d5 | ||
|
|
f1ee168657 | ||
|
|
5fef60c2b0 | ||
|
|
4afffc7dd3 | ||
|
|
7e6346a694 | ||
|
|
abf22eff44 | ||
|
|
3fa3b93c85 | ||
|
|
549ef9dabc | ||
|
|
59c75663b1 | ||
|
|
820a5bc363 | ||
|
|
1b9cf631be | ||
|
|
b4f2208bae | ||
|
|
f4bcfca323 | ||
|
|
f93a9a0f22 | ||
|
|
e5652197eb | ||
|
|
4a102d44cb | ||
|
|
c837840e04 | ||
|
|
b280ff7495 | ||
|
|
f6d8dcf6fd | ||
|
|
fa0661f58a | ||
|
|
e2fe137b05 | ||
|
|
b434e955ac | ||
|
|
d74b302edb | ||
|
|
0200430346 | ||
|
|
d70ebc2398 | ||
|
|
2b606a2dec | ||
|
|
9c7f2250b9 | ||
|
|
c2ee621f64 | ||
|
|
b2cdb46c84 | ||
|
|
6d150aa5cb | ||
|
|
62ece66f36 | ||
|
|
628cd3896c | ||
|
|
f10418face | ||
|
|
ca9a629804 | ||
|
|
bb30535afb | ||
|
|
624f863da4 | ||
|
|
b55a9f253e | ||
|
|
5b9ef5b6b6 | ||
|
|
e7c8ecbd31 | ||
|
|
592dfec8db | ||
|
|
23ebccc041 | ||
|
|
036bd51298 | ||
|
|
6d9a66cc41 | ||
|
|
1923b84a01 | ||
|
|
9924e293c9 | ||
|
|
490d3549e2 | ||
|
|
45d2a5d0b6 | ||
|
|
4d3929948c | ||
|
|
56ea09431f | ||
|
|
a53a5f4685 | ||
|
|
52f3ff5ff6 | ||
|
|
7150783848 | ||
|
|
c03d3520d6 | ||
|
|
d2e19c5129 | ||
|
|
a829165f2d | ||
|
|
f2707d053d | ||
|
|
2a4ccf69b2 | ||
|
|
818356dfed | ||
|
|
49d6743cbb | ||
|
|
9ed80d46b6 | ||
|
|
c2f5a6390e | ||
|
|
f2a7824168 | ||
|
|
3439861f74 | ||
|
|
06ee096746 | ||
|
|
6230a7553d | ||
|
|
e17b07bb12 | ||
|
|
ea2d4e9206 | ||
|
|
2fffc86a5a | ||
|
|
1bb0af72ee | ||
|
|
23a58ac064 | ||
|
|
af9d16852e | ||
|
|
ee47c1ea10 | ||
|
|
2b318152fa | ||
|
|
10a363248e | ||
|
|
11d4bde18a | ||
|
|
b88b992cb6 | ||
|
|
6e3e1b56fb | ||
|
|
7dfda2598d | ||
|
|
853862c475 | ||
|
|
5627bb6bed | ||
|
|
b646e69b6b | ||
|
|
632aeed00b | ||
|
|
3f5b4bad62 | ||
|
|
7bba4ed820 | ||
|
|
e22ff1bbfe | ||
|
|
ab66567db6 | ||
|
|
a763e1729c | ||
|
|
6aac250990 | ||
|
|
a749b97707 | ||
|
|
f966b23f3a | ||
|
|
763025d19b | ||
|
|
0bf2ae6075 | ||
|
|
71f947484e | ||
|
|
5160164111 | ||
|
|
7501e029ab | ||
|
|
a678555d8d | ||
|
|
7ce2991b0f | ||
|
|
befa396e82 | ||
|
|
fb69fc5af2 | ||
|
|
b540b5813e | ||
|
|
feb74d90f6 | ||
|
|
a0de2577e8 | ||
|
|
dbc5112ada | ||
|
|
9f8335810c | ||
|
|
c54e2388ce | ||
|
|
a8a7019411 | ||
|
|
098da3c3dd | ||
|
|
71b5645801 | ||
|
|
f5d9fbe91c | ||
|
|
420e15c179 | ||
|
|
74619f6f8d | ||
|
|
1355a4a28d | ||
|
|
97c34b889a | ||
|
|
0b0c54d874 | ||
|
|
1005be006f | ||
|
|
8db113a19b | ||
|
|
075df8a26d | ||
|
|
38cf3f40e1 | ||
|
|
4a0abbbee7 | ||
|
|
15f1201a76 | ||
|
|
b152723ed2 | ||
|
|
84a2832a65 | ||
|
|
8037494f7a | ||
|
|
97c1ace020 | ||
|
|
64457b0235 | ||
|
|
67ef831681 | ||
|
|
1fd6aae3d9 | ||
|
|
61810cc977 | ||
|
|
59401e18ed | ||
|
|
30eff93fa1 | ||
|
|
7c5bae3b53 | ||
|
|
ee16e4236e | ||
|
|
30e9cf9dc8 | ||
|
|
ac5d0bf8a3 | ||
|
|
923eb05e59 | ||
|
|
8f59e51445 | ||
|
|
766733617e | ||
|
|
d77744c562 | ||
|
|
0d6db1305e | ||
|
|
61c2e59f41 | ||
|
|
47dd7adf4b | ||
|
|
016736c455 | ||
|
|
6d3924ba43 | ||
|
|
428f963243 | ||
|
|
dd871b64ea | ||
|
|
38863f618a | ||
|
|
8023285b9d | ||
|
|
1aa7175006 | ||
|
|
1222c30738 | ||
|
|
0c8e62add9 | ||
|
|
eb1d06b4a6 | ||
|
|
d58c3292d7 | ||
|
|
4320d26a3d | ||
|
|
3ca4e33d94 | ||
|
|
19e726a630 | ||
|
|
96dddef271 | ||
|
|
34a228f85e | ||
|
|
213d996168 | ||
|
|
5a159ce01f | ||
|
|
fed9c64113 | ||
|
|
2d835581a5 | ||
|
|
c8f1ebdf4c | ||
|
|
98e3530acd | ||
|
|
1a5b216dd5 | ||
|
|
ae98d5e3bd | ||
|
|
750825b3c3 | ||
|
|
8c255256c9 | ||
|
|
19626361ec | ||
|
|
df4bd1fa4a | ||
|
|
62bf5abd8d | ||
|
|
cd9ec9f346 | ||
|
|
cf7d5b3481 | ||
|
|
12f9ac3aa4 | ||
|
|
4519cdb49c | ||
|
|
d20b6f355c | ||
|
|
70e64003f9 | ||
|
|
0a4644e743 | ||
|
|
c428d23d8b | ||
|
|
d6b189badc | ||
|
|
6e899391c0 | ||
|
|
e0acbcc32d | ||
|
|
95fb9ea117 | ||
|
|
e80b7cf0a2 | ||
|
|
5e70c06075 | ||
|
|
1413b74f76 | ||
|
|
bf0548e802 | ||
|
|
b7e1863526 | ||
|
|
f189188563 | ||
|
|
2f52664820 | ||
|
|
5f6fa73be9 | ||
|
|
b7ec913cb9 | ||
|
|
ebef4b079c | ||
|
|
a81e5c4e6b | ||
|
|
b0733dcd51 | ||
|
|
e9bd35619d | ||
|
|
6528b34152 | ||
|
|
b60c02e0c7 | ||
|
|
a0792d166b | ||
|
|
fcf36c4bc0 | ||
|
|
e5b617cd16 | ||
|
|
0acefb4521 | ||
|
|
111c8367a9 | ||
|
|
ead8f209b6 | ||
|
|
96333b616b | ||
|
|
5698e0deda | ||
|
|
df2ddebf6c | ||
|
|
71ab7528e7 | ||
|
|
b4e459d831 | ||
|
|
a57d3fdf3f | ||
|
|
2c207873be | ||
|
|
fc8385113f | ||
|
|
95d7d26f11 | ||
|
|
43a13964bd | ||
|
|
d2053d2db7 | ||
|
|
8ba2bcaa53 | ||
|
|
f7abdbe97f | ||
|
|
91af3e60ba | ||
|
|
8fe196cd7a | ||
|
|
66d7241c03 | ||
|
|
89d7c0b0d0 | ||
|
|
d2ec62d681 | ||
|
|
b6d38fe8f1 | ||
|
|
1edc256148 | ||
|
|
24ac385898 | ||
|
|
f062e58f7b | ||
|
|
96aec401b9 | ||
|
|
7ff0b7aa3c | ||
|
|
e5ab5241d5 | ||
|
|
0f4f87067e | ||
|
|
3f32f816b0 | ||
|
|
73de2dfda7 | ||
|
|
d6fd6cb5a3 | ||
|
|
39fbbe896f | ||
|
|
29c70acf4e | ||
|
|
5cd2568776 | ||
|
|
60a6535a12 | ||
|
|
f48b389449 | ||
|
|
316dd210a0 | ||
|
|
a60712c09d | ||
|
|
482cd564ff | ||
|
|
ac1171d43b | ||
|
|
ed8953c430 | ||
|
|
9a8aecaf3f | ||
|
|
423719e7bc | ||
|
|
7f2b6a874e | ||
|
|
cfe5ea3f9b | ||
|
|
07aa058a46 | ||
|
|
6cadf93c43 | ||
|
|
60eb1332d2 | ||
|
|
a9ee7e93fd | ||
|
|
2782216e52 | ||
|
|
d22537c5f2 | ||
|
|
57aa6c19e1 | ||
|
|
761553d392 | ||
|
|
29350ab7b0 | ||
|
|
528ccc1e9d | ||
|
|
20d26ad7ca | ||
|
|
5d23c5c902 | ||
|
|
145794bf04 | ||
|
|
d00f2aa8d0 | ||
|
|
3a20375567 | ||
|
|
7be93a8a44 | ||
|
|
b5e4c4e92a | ||
|
|
20285796bd | ||
|
|
7826ff94e3 | ||
|
|
f1dccbb64d | ||
|
|
528e301ce4 | ||
|
|
af016a9c79 | ||
|
|
cbd5738543 | ||
|
|
2dd0899a3d | ||
|
|
e486a4baef | ||
|
|
5fc11baf9e | ||
|
|
157777cac1 | ||
|
|
99d0ee6725 | ||
|
|
b5c1051506 | ||
|
|
bba3334df5 | ||
|
|
74488feec2 | ||
|
|
54953abc67 | ||
|
|
117bbdbcdf | ||
|
|
b96b99c1c4 | ||
|
|
6e856a7648 | ||
|
|
0659edb762 | ||
|
|
dcb870c432 | ||
|
|
772bafbe43 | ||
|
|
a9be6aff44 | ||
|
|
dcd7ec7383 | ||
|
|
c69a4dda00 | ||
|
|
a911926119 | ||
|
|
6f30aec4f2 | ||
|
|
5a005fb809 | ||
|
|
776a4c5dce | ||
|
|
c53c316303 | ||
|
|
622aa844e4 | ||
|
|
de2cf6026e | ||
|
|
a8e02b9ced | ||
|
|
297308ad76 | ||
|
|
ea0c3dbe5a | ||
|
|
b8d229e58e | ||
|
|
c4f5110148 | ||
|
|
7fdd7e89bd | ||
|
|
2378346537 | ||
|
|
72fc5fc3b1 | ||
|
|
c063c99ba6 | ||
|
|
90341f0a6e | ||
|
|
cdb9df5aba | ||
|
|
1f6d9d6422 | ||
|
|
ffbda7e521 | ||
|
|
3b5ef29047 | ||
|
|
14cf6ceb84 | ||
|
|
5fb940ff2a | ||
|
|
f446e18289 | ||
|
|
84f26b32d6 | ||
|
|
f7690245aa | ||
|
|
f44e32fd6a | ||
|
|
8bac34238e | ||
|
|
6d2f6ce2f9 | ||
|
|
3a465cc56b | ||
|
|
617369dbc0 | ||
|
|
c0fed1498e | ||
|
|
5bdd3ce47a | ||
|
|
6b3f41d675 | ||
|
|
23b696c9cf | ||
|
|
079400f89e | ||
|
|
e12d467627 | ||
|
|
162ca3e21e | ||
|
|
dddd0e7b71 | ||
|
|
95d68e09da | ||
|
|
aaf0cf53d8 | ||
|
|
9c8f759732 | ||
|
|
a45c685893 | ||
|
|
87bdebb21c | ||
|
|
4f754ae309 | ||
|
|
4b004f70ec | ||
|
|
d468d4c21b | ||
|
|
a4df433d80 | ||
|
|
10eec025d2 | ||
|
|
d497ed4195 | ||
|
|
e63137d293 | ||
|
|
c744743913 | ||
|
|
42493c8eb6 | ||
|
|
391839028f | ||
|
|
d9ecfeadc0 | ||
|
|
d866646f66 | ||
|
|
6295041341 | ||
|
|
8c7556427a | ||
|
|
82c91db78c | ||
|
|
2d969f4fff | ||
|
|
e84d46dae7 | ||
|
|
b6828b54ca | ||
|
|
f9bd1bac36 | ||
|
|
22e2bfacae | ||
|
|
c446d4bb54 | ||
|
|
23c7e5dc3f | ||
|
|
661f1e624c | ||
|
|
81ff5ef899 | ||
|
|
e79364cb03 | ||
|
|
d750e2fe7a | ||
|
|
5e1025453a | ||
|
|
280da481ee | ||
|
|
9da5f47623 | ||
|
|
45f1f419e1 | ||
|
|
92f2ac67d5 | ||
|
|
d28a62d70b | ||
|
|
f9336f2a28 | ||
|
|
940e67b1ca | ||
|
|
073e138ab2 | ||
|
|
5aec4b4571 | ||
|
|
f9cd3decb1 | ||
|
|
627c47b155 | ||
|
|
57135ea2c6 | ||
|
|
609e9fcdb0 | ||
|
|
5b0e71b680 | ||
|
|
9c2d478797 | ||
|
|
c55fa13038 | ||
|
|
27b9565d2f | ||
|
|
4fe6d79fff | ||
|
|
e636e38ba1 | ||
|
|
ebc6665224 | ||
|
|
7001cedbc7 | ||
|
|
b14209d5cf | ||
|
|
5150564fe2 | ||
|
|
b7eaa9e353 | ||
|
|
c00943591d | ||
|
|
1f9320200a | ||
|
|
6a6b80cce2 | ||
|
|
05296e3d9b | ||
|
|
7e68050e0a | ||
|
|
ab928be1b3 | ||
|
|
65d26d753d | ||
|
|
bf37c09ba0 | ||
|
|
89199b81ab | ||
|
|
0dd17673f5 | ||
|
|
c17d6c2334 | ||
|
|
5285dd1665 | ||
|
|
046ce30e08 | ||
|
|
1601fa5608 | ||
|
|
5f7099184d | ||
|
|
8425bb4f59 | ||
|
|
e44006f531 | ||
|
|
3423e24de6 | ||
|
|
5ac363232f | ||
|
|
9cc020a2c7 | ||
|
|
d2240f07d8 | ||
|
|
4968db750b | ||
|
|
6134244244 | ||
|
|
4559ca9f2b | ||
|
|
9a38920cb8 | ||
|
|
2b771931e6 | ||
|
|
d72e003f8c | ||
|
|
097988e046 | ||
|
|
4d15bc7ea0 | ||
|
|
26f49e2877 | ||
|
|
10aba86e70 | ||
|
|
9e3d100599 | ||
|
|
a7193e321c | ||
|
|
fa15469696 | ||
|
|
58b9cdf28f | ||
|
|
8e05fe3b0c | ||
|
|
af063b2e9e | ||
|
|
5cc85cc860 | ||
|
|
eafa1eabee | ||
|
|
34a1838668 | ||
|
|
df83c94180 | ||
|
|
e102b60923 | ||
|
|
02900eaa6d | ||
|
|
5ed4c51582 | ||
|
|
81e928f94e | ||
|
|
985b569d29 | ||
|
|
d2d000ef16 | ||
|
|
520b3a14bc | ||
|
|
157d194cc5 | ||
|
|
2785609481 | ||
|
|
6e5e60173b | ||
|
|
f37e938f17 | ||
|
|
da645acd1c | ||
|
|
17205b2baf | ||
|
|
b5ba4d3570 | ||
|
|
17b24d3c24 | ||
|
|
044454dca2 | ||
|
|
88bff9ab6c | ||
|
|
203fde60d6 | ||
|
|
82956c4149 | ||
|
|
1f41b9e481 | ||
|
|
945921fa9a | ||
|
|
7d5786ea93 | ||
|
|
6be1413d7d | ||
|
|
fd07ab10ee | ||
|
|
6232656ad4 | ||
|
|
8493c7ffe5 | ||
|
|
15700b85cb | ||
|
|
3dfd1c98ba | ||
|
|
9a249b0dec | ||
|
|
b74a431ac9 | ||
|
|
880ce18fd0 | ||
|
|
6279149cb8 | ||
|
|
f5c5a34798 | ||
|
|
e9a616c68d | ||
|
|
f5ee7160cb | ||
|
|
cea671aab5 | ||
|
|
da84cde6da | ||
|
|
e9fbce4e28 | ||
|
|
913605a065 | ||
|
|
4bf49df6fa | ||
|
|
91a9d6c68f | ||
|
|
a477c3c4d9 | ||
|
|
0cdd56e0ac | ||
|
|
abefb894cc | ||
|
|
97d482c1ad | ||
|
|
d3e9303d6d | ||
|
|
df7bb13752 | ||
|
|
d28f6f5922 | ||
|
|
c90ad7c1e2 | ||
|
|
7fbdcb8a88 | ||
|
|
d46daed49a | ||
|
|
f18a03ee6d | ||
|
|
1d052e7c1b | ||
|
|
2611165f21 | ||
|
|
f059aa7407 | ||
|
|
ac27df1f0e | ||
|
|
76b28593ea | ||
|
|
0940c88c20 | ||
|
|
c3408040fc | ||
|
|
d2ffc11749 | ||
|
|
4d640ec467 | ||
|
|
c409d49f14 | ||
|
|
2c0dbf1062 | ||
|
|
25f0208e61 | ||
|
|
d063cfe36a | ||
|
|
5c089e1d77 | ||
|
|
867006d29c | ||
|
|
6a974c48ef | ||
|
|
c314918c6b | ||
|
|
e2e2a076c7 | ||
|
|
8ee12b9f26 | ||
|
|
7377293f81 | ||
|
|
29ae49b5f1 | ||
|
|
195d967b3f | ||
|
|
eac74bf9c1 | ||
|
|
9f2dbf7b6c | ||
|
|
9e836ba586 | ||
|
|
cc6dc1b3a2 | ||
|
|
f49da2c9bf | ||
|
|
96c1077238 | ||
|
|
8d72b27e1d | ||
|
|
0ea0d139dd | ||
|
|
b81ff4d672 | ||
|
|
f380ac5e43 | ||
|
|
962d42292d | ||
|
|
15df15556d | ||
|
|
6b29841cc8 | ||
|
|
4f4c1a9bb8 | ||
|
|
5f7630b906 | ||
|
|
8a831889f9 | ||
|
|
bce133ac28 | ||
|
|
f5215d715a | ||
|
|
fde0f3bba1 | ||
|
|
e7b18bd3a2 | ||
|
|
e5e86e639a | ||
|
|
f44b44a354 | ||
|
|
b3399b5242 | ||
|
|
7d4ebd9d3b | ||
|
|
3bb2131375 | ||
|
|
d7314ec2a4 | ||
|
|
cc6c724ee8 | ||
|
|
d3b0559b72 | ||
|
|
1e24caec31 | ||
|
|
65cdc143da | ||
|
|
5d612f020c | ||
|
|
ccef2cc178 | ||
|
|
9337160583 | ||
|
|
bf9d570c3d | ||
|
|
306b0096be | ||
|
|
45583ea469 | ||
|
|
15c6c372ba | ||
|
|
770a89507a | ||
|
|
ddc9aa7506 | ||
|
|
a7d9fd19d9 | ||
|
|
091f7c49ab | ||
|
|
b443f59078 | ||
|
|
27bcf92e9b | ||
|
|
31100c3d82 | ||
|
|
119da2e76e | ||
|
|
588a6cf74f | ||
|
|
eb6394eb6a | ||
|
|
76de183ec2 | ||
|
|
ba31ceb3e7 | ||
|
|
e94e0f8a6b | ||
|
|
f8283acfae | ||
|
|
f8cb26ca74 | ||
|
|
190b9da6c7 | ||
|
|
f84b46148c | ||
|
|
12db8b5ee1 | ||
|
|
05b5078aa9 | ||
|
|
85b7ee85f3 | ||
|
|
326b728d4b | ||
|
|
2e45e131b1 | ||
|
|
1aa95c057b | ||
|
|
6de7a849b3 | ||
|
|
268091b10e | ||
|
|
3920c85ab7 | ||
|
|
524565f0bb | ||
|
|
69c1c856d9 | ||
|
|
dd62d92ffb | ||
|
|
f7e89d75a4 | ||
|
|
023f31eadd | ||
|
|
da8df5beac | ||
|
|
f3a8825cb9 | ||
|
|
835fd47482 | ||
|
|
efbd5cab85 | ||
|
|
a6b7d0bcc5 | ||
|
|
e06126d889 | ||
|
|
4bf8e2c488 | ||
|
|
1c55ad21a3 | ||
|
|
3a601e1e65 | ||
|
|
c953003c2f | ||
|
|
18de51a531 | ||
|
|
ab6d3b5e8d | ||
|
|
151980c6de | ||
|
|
375527b765 | ||
|
|
2978e567d4 | ||
|
|
8ad50ab61c | ||
|
|
2145ded2f2 | ||
|
|
29c4d9f4d6 | ||
|
|
c7de3d299a | ||
|
|
8bad476315 | ||
|
|
bc8eb44a53 | ||
|
|
f98e22cb76 | ||
|
|
5b6326e462 | ||
|
|
342f249fab | ||
|
|
09ba6d834a | ||
|
|
61654f815d | ||
|
|
bf450766b2 | ||
|
|
2f813f3d91 | ||
|
|
51e46db42d | ||
|
|
11e0dd18d3 | ||
|
|
ff5b024074 | ||
|
|
2f53c1a860 | ||
|
|
53b1544b58 | ||
|
|
846fc9008c | ||
|
|
cf7455c661 | ||
|
|
ea52bbea42 | ||
|
|
712c41d927 | ||
|
|
c33da4a5ae | ||
|
|
10aecb9390 | ||
|
|
a1eafe311e | ||
|
|
df416be43e | ||
|
|
08035bf8a5 | ||
|
|
05a990e228 | ||
|
|
4d7d1699f9 | ||
|
|
92b0ebb6f6 | ||
|
|
e41accf52d | ||
|
|
1cca60fa53 | ||
|
|
69f489ffc5 | ||
|
|
903e305519 | ||
|
|
9ed3e8befb | ||
|
|
cd38c99f7e | ||
|
|
3fc26733ad | ||
|
|
e24134ff6f | ||
|
|
901063f4c9 | ||
|
|
be8742f69e | ||
|
|
dbd6b4bd52 | ||
|
|
8a39e8094c | ||
|
|
a154a6cce5 | ||
|
|
052ec14a6b | ||
|
|
fa9034d57b | ||
|
|
266adf788c | ||
|
|
b19aedd17c | ||
|
|
f959543c19 | ||
|
|
0a6c3baf24 | ||
|
|
5a33c1eed6 | ||
|
|
ce1196e17a | ||
|
|
a9fd5a3162 | ||
|
|
18b33a7776 | ||
|
|
b72fe0d7a2 | ||
|
|
551e5a0a25 | ||
|
|
92d4a580c1 | ||
|
|
b367701a96 | ||
|
|
8595863afe | ||
|
|
21492ed88e | ||
|
|
4dc14ab7f9 | ||
|
|
5caf3409db | ||
|
|
1565c32162 | ||
|
|
45edb4e5da | ||
|
|
5bf1c4f433 | ||
|
|
3cc692d3fb | ||
|
|
e42b2490f0 | ||
|
|
454b1f69ed | ||
|
|
b410756dfd | ||
|
|
1458919549 | ||
|
|
48ae8c2465 | ||
|
|
0a78bcb374 | ||
|
|
61cdb48273 | ||
|
|
b3350b22b6 | ||
|
|
d35d22c7d8 | ||
|
|
24cd11152b | ||
|
|
d21254ac02 | ||
|
|
70f08c806a | ||
|
|
e7c3fb02e8 | ||
|
|
3d3cf1d76e | ||
|
|
2bf385fe38 | ||
|
|
7ba595be55 | ||
|
|
c45e79c588 | ||
|
|
f37568b050 | ||
|
|
b5afc1cd1c | ||
|
|
e9777ccfc6 | ||
|
|
898404fc65 | ||
|
|
131212b158 | ||
|
|
3f1d3149e9 | ||
|
|
bfc8b199b6 | ||
|
|
6d4b487428 | ||
|
|
9337201ffb | ||
|
|
494b2c6786 | ||
|
|
bc1c8032c1 | ||
|
|
21b0a4d370 | ||
|
|
133effccfc | ||
|
|
62b4ebc4a9 | ||
|
|
12941ea19e | ||
|
|
f94bd706a4 | ||
|
|
3cbbc29c00 | ||
|
|
0827c18eeb | ||
|
|
6c4ebc9f58 | ||
|
|
1f2bfe8245 | ||
|
|
305d7485c1 | ||
|
|
4ded05bbd1 | ||
|
|
540a2b1876 | ||
|
|
153d3ad388 | ||
|
|
a3e36d2453 | ||
|
|
b9449a798b | ||
|
|
9da149a868 | ||
|
|
d505c00403 | ||
|
|
4d7a0a361f | ||
|
|
e08e02ae80 | ||
|
|
95c6f569d6 | ||
|
|
e46759f436 | ||
|
|
b42dd5289b | ||
|
|
a911a007d2 | ||
|
|
64babe2e42 | ||
|
|
099c94c215 | ||
|
|
75b81a0fd2 | ||
|
|
f9ab5d4013 | ||
|
|
b83080e2d7 | ||
|
|
6a21106347 | ||
|
|
9a7d8c858d | ||
|
|
8339c0d8de | ||
|
|
2b1136ea02 | ||
|
|
84b4d69913 | ||
|
|
3fe9ce378e | ||
|
|
57b9571d86 | ||
|
|
ae3071d318 | ||
|
|
8a93814bac | ||
|
|
a6dd4345ab | ||
|
|
c71456444f | ||
|
|
b916605a24 | ||
|
|
553da1e7e8 | ||
|
|
847651ead7 | ||
|
|
f977f261d6 | ||
|
|
3fa9e89e8e | ||
|
|
0662959e1d | ||
|
|
e5e03f9693 | ||
|
|
4203900365 | ||
|
|
eb7794ba47 | ||
|
|
9626f33768 | ||
|
|
cfc0ace41e | ||
|
|
ce2947c756 | ||
|
|
87fc10ad24 | ||
|
|
cae71559a0 | ||
|
|
3cf7920a22 | ||
|
|
fba9b46fe9 | ||
|
|
611f074a9d | ||
|
|
7909703f4c | ||
|
|
dcbf4b8faf | ||
|
|
c5edcf47bd | ||
|
|
02e6b89fdd | ||
|
|
c4109a19d6 | ||
|
|
630d9492cd | ||
|
|
b762d95622 | ||
|
|
3738997832 | ||
|
|
21c70039f4 | ||
|
|
23e3385290 | ||
|
|
4ab82c99a8 | ||
|
|
f4df37da23 | ||
|
|
4494d8652d | ||
|
|
32ae4393e2 | ||
|
|
ea5c3a7c5e | ||
|
|
f9d9af4fe9 | ||
|
|
098ef61b5d | ||
|
|
e926f56f6b | ||
|
|
9b1da3cfa0 | ||
|
|
1fbcd9b362 | ||
|
|
38940e0111 | ||
|
|
4fa3570d1e | ||
|
|
d1c78d5062 | ||
|
|
c4862bdddf | ||
|
|
2b8018727c | ||
|
|
e3be279f1f | ||
|
|
1e6126d5be | ||
|
|
9a09708842 | ||
|
|
e861204cb0 | ||
|
|
afd3afcf0d | ||
|
|
5055b0c75d | ||
|
|
372104cdfe | ||
|
|
acb24fd265 | ||
|
|
5b7420ba90 | ||
|
|
e73dbd5c15 | ||
|
|
b5f82beb46 | ||
|
|
61b97fd09b | ||
|
|
99e34860d4 | ||
|
|
5d44bbe956 | ||
|
|
e7d0b575bb | ||
|
|
8b2a535f19 | ||
|
|
a242dba345 | ||
|
|
587cb5de16 | ||
|
|
e93c6957ac | ||
|
|
f644115b54 | ||
|
|
0c753d22b6 | ||
|
|
ec7f2c33e7 | ||
|
|
39c1c1e371 | ||
|
|
74d5faf3fa | ||
|
|
15204a2c84 | ||
|
|
2397cb5428 | ||
|
|
4b6b87d632 | ||
|
|
2492b8de34 | ||
|
|
635987a420 | ||
|
|
51602ed231 | ||
|
|
25aab0f702 | ||
|
|
23b3c7f1fd | ||
|
|
451ce74fa4 | ||
|
|
1fd9609810 | ||
|
|
29804e0a2b | ||
|
|
26aa7e8332 | ||
|
|
e4e00be119 | ||
|
|
de6b71528b | ||
|
|
d005ace383 | ||
|
|
f566e10710 | ||
|
|
18f9c6b1f0 | ||
|
|
fbf4de0ec5 | ||
|
|
3d94122abc | ||
|
|
442a66df2e | ||
|
|
3be5d61ced | ||
|
|
f137e23b43 | ||
|
|
f00178cc0d | ||
|
|
e33c5b055d | ||
|
|
f2237a385e | ||
|
|
a9c45f7e78 | ||
|
|
11cfe5ee82 | ||
|
|
4cbcee85d6 | ||
|
|
98ec2cceb4 | ||
|
|
8ce05c8bbe | ||
|
|
a7019b2e60 | ||
|
|
0facdc0497 | ||
|
|
25a7560e2e | ||
|
|
063d909572 | ||
|
|
2f8e112f3a | ||
|
|
99abfd0d98 | ||
|
|
5fa9a27ee0 | ||
|
|
b07d675bb4 | ||
|
|
9f75c37331 | ||
|
|
df96b05863 | ||
|
|
d6adfea9b1 | ||
|
|
389b439e9a | ||
|
|
046b89fa21 | ||
|
|
72e5532c6c | ||
|
|
5688d85789 | ||
|
|
28b63e08f1 | ||
|
|
951ce77853 | ||
|
|
b37ba63018 | ||
|
|
251d251661 | ||
|
|
e11750fb75 | ||
|
|
1634ddeb25 | ||
|
|
7d4bcd7f15 | ||
|
|
13d9b6cc5a | ||
|
|
8d0c41baa0 | ||
|
|
0303c96ee1 | ||
|
|
fde6d7921e | ||
|
|
c632d8ebec | ||
|
|
31b43e8754 | ||
|
|
195360a0f9 | ||
|
|
f293f88958 | ||
|
|
6ccfab4087 | ||
|
|
a45ce55808 | ||
|
|
c7dabe1b6f | ||
|
|
ec51268439 | ||
|
|
7543b9fa37 | ||
|
|
ca3187d0b8 | ||
|
|
327cd93e3c | ||
|
|
13853c708e | ||
|
|
ee1291c816 | ||
|
|
6d2d3ae528 | ||
|
|
784f94ecdb | ||
|
|
93bf853b5e | ||
|
|
bb83ddfe28 | ||
|
|
b51ec53e33 | ||
|
|
ca210f2b6d | ||
|
|
38bddec4ba | ||
|
|
b866d57814 | ||
|
|
3c9004d87d | ||
|
|
c479dd404c | ||
|
|
748667a0b4 | ||
|
|
6898595f8a | ||
|
|
30d0b6fd0e | ||
|
|
7c209db146 | ||
|
|
49c8c88a22 | ||
|
|
88e530c96c | ||
|
|
14f3fb5a94 | ||
|
|
7ac479b78a | ||
|
|
10b356e642 | ||
|
|
7f92482d7a | ||
|
|
b79a7309aa | ||
|
|
b54781ff56 | ||
|
|
6a87495a6d | ||
|
|
c5d9346370 | ||
|
|
d247e2c111 | ||
|
|
b30f47bac4 | ||
|
|
2f9498e137 | ||
|
|
067b3513b7 | ||
|
|
e50ed22c85 | ||
|
|
7cf17f3cc4 | ||
|
|
9f52ecab5c | ||
|
|
c8a56d4f78 | ||
|
|
71d482ab29 | ||
|
|
1cc7b46555 | ||
|
|
f56a65d30d | ||
|
|
aff813b284 | ||
|
|
181c0e8a60 | ||
|
|
cf7f614296 | ||
|
|
351e37bcee | ||
|
|
cc1f27f588 | ||
|
|
859905c3e4 | ||
|
|
8af91bffb5 | ||
|
|
06dc8ccbdd | ||
|
|
c501a417bb | ||
|
|
0021e229d8 | ||
|
|
b4ef95a9b4 | ||
|
|
f25a716d62 | ||
|
|
a9739ed500 | ||
|
|
a131eeaa4a | ||
|
|
9382bbd8fd | ||
|
|
adb1e292bf | ||
|
|
ca79929141 | ||
|
|
ae2998bcf2 | ||
|
|
1e8e09d5c4 | ||
|
|
a5e30bc818 | ||
|
|
72edf5c08b | ||
|
|
192154a11c | ||
|
|
c3700cf6d9 | ||
|
|
78bdee61ef | ||
|
|
b84eea9620 | ||
|
|
5f289fa400 | ||
|
|
67b8f468e4 | ||
|
|
9c49c84306 | ||
|
|
b31ee802fc | ||
|
|
b893a0eb76 | ||
|
|
6f1a04abce | ||
|
|
041ba27efe | ||
|
|
e239036d8b | ||
|
|
d3f073e573 | ||
|
|
0b7490dc06 | ||
|
|
a0e514dac9 | ||
|
|
2f1eaf7d6b | ||
|
|
6cb8b1c439 | ||
|
|
5363208e4e | ||
|
|
510ff51198 | ||
|
|
6dde3d55ef | ||
|
|
e3ec53c2d0 | ||
|
|
e7972d4903 | ||
|
|
a2c3b5d64e | ||
|
|
b11d653fc0 | ||
|
|
66792f2d56 | ||
|
|
c012ead143 | ||
|
|
82906aee58 | ||
|
|
7ff4a82755 | ||
|
|
8ca49c1e18 | ||
|
|
7d68a57f53 | ||
|
|
c68487c0c7 | ||
|
|
4adc660705 | ||
|
|
d78e73bd6f | ||
|
|
9d33690f34 | ||
|
|
4c428e5b5b | ||
|
|
4c3882689f | ||
|
|
c82ed473fc | ||
|
|
06664b4c58 | ||
|
|
5e68388b01 | ||
|
|
e14fcf8577 | ||
|
|
0219c5253b | ||
|
|
adf3d74d91 | ||
|
|
3acd68e0b3 | ||
|
|
e5eccd732d | ||
|
|
eb0df5791a | ||
|
|
f5371123da | ||
|
|
6aa723bc22 | ||
|
|
9ba34df4ae | ||
|
|
6194515f8e | ||
|
|
39289715bc | ||
|
|
5fdd2430ca | ||
|
|
a7f3f485ad | ||
|
|
69a76fa1b7 | ||
|
|
2e5e64b05d | ||
|
|
933e3233a7 | ||
|
|
a54df29542 | ||
|
|
cdce802b32 | ||
|
|
2abf30e94b | ||
|
|
148cff1b48 | ||
|
|
ce2a21c438 | ||
|
|
d77c0198d1 | ||
|
|
f58a1acff5 | ||
|
|
aadbddd7e9 | ||
|
|
689fe3947b | ||
|
|
fff0b8b187 | ||
|
|
74562432cf | ||
|
|
8e3027642b | ||
|
|
39f96bb12c | ||
|
|
938309d125 | ||
|
|
f740b69ffe | ||
|
|
0a4147aa0e | ||
|
|
dcffc13843 | ||
|
|
1e9a0cdc16 | ||
|
|
82e7050864 | ||
|
|
72d1e55373 | ||
|
|
fe5d5df2d7 | ||
|
|
9bc337373e | ||
|
|
5aad879a95 | ||
|
|
a3798dba68 | ||
|
|
b9f7ef5cbd | ||
|
|
0c3b541031 | ||
|
|
a09bc53b99 | ||
|
|
3731723472 | ||
|
|
ded29619cd | ||
|
|
a5b39a8f17 | ||
|
|
26866a7b2c | ||
|
|
7837f3999f | ||
|
|
b25f658647 | ||
|
|
49625619fe | ||
|
|
912299bcfd | ||
|
|
5648fd2e91 | ||
|
|
557ef5820e | ||
|
|
4ce512d259 | ||
|
|
a68319dae4 | ||
|
|
080ecf51d3 | ||
|
|
657109dae1 | ||
|
|
019ef02be8 | ||
|
|
d4774c963d | ||
|
|
34cb4c579c | ||
|
|
74c261f913 | ||
|
|
7420123519 | ||
|
|
374910736e | ||
|
|
3a71696a49 | ||
|
|
73792905a2 | ||
|
|
05fc30e6e8 | ||
|
|
9c308588b5 | ||
|
|
2f53200096 | ||
|
|
8c1f2c6064 | ||
|
|
f5fc2acf50 | ||
|
|
306fa24d6b | ||
|
|
f5ee9d4a3b | ||
|
|
e7a5f64fe5 | ||
|
|
6191e003fc | ||
|
|
fad401941e | ||
|
|
1e0733bd46 | ||
|
|
d0a44c3f14 | ||
|
|
3cee0c1bd5 | ||
|
|
f5d403e97d | ||
|
|
4f1d021aa8 | ||
|
|
dca8c042ab | ||
|
|
ae1ccadcc8 | ||
|
|
9ac12c2532 | ||
|
|
18337c97e2 | ||
|
|
1e652d497e | ||
|
|
b53cad2808 | ||
|
|
4520ff78ff | ||
|
|
b887129cd7 | ||
|
|
ec25831a37 | ||
|
|
744f74b498 | ||
|
|
52aaf93f37 | ||
|
|
2d92d4ad87 | ||
|
|
7617cc0a80 | ||
|
|
dc69bcf6f2 | ||
|
|
0775fc7ead | ||
|
|
034aef483b | ||
|
|
ee5b99fed4 | ||
|
|
e031da1337 | ||
|
|
8f1514642c | ||
|
|
5aa304ea9a | ||
|
|
c5a27b2cc7 | ||
|
|
0fde404da8 | ||
|
|
5242b9af39 | ||
|
|
5e2d6fc05f | ||
|
|
7e08a1f321 | ||
|
|
076295eae8 | ||
|
|
c13339ca52 | ||
|
|
627657e1de | ||
|
|
a8349671d0 | ||
|
|
461875b0e4 | ||
|
|
00bbb6bc6e | ||
|
|
e1f1181a07 | ||
|
|
0e1de39192 | ||
|
|
05bbeb10da | ||
|
|
7375a9e06b | ||
|
|
4910050891 | ||
|
|
67d4f666ce | ||
|
|
e6c9449e3c | ||
|
|
b8effba497 | ||
|
|
8fcdd7cb8a | ||
|
|
f3fb5ccc3b | ||
|
|
b8f55f982f | ||
|
|
6db59cb896 | ||
|
|
3db83c1602 | ||
|
|
6be9225fbd | ||
|
|
653eff403c | ||
|
|
ab410ec0cf | ||
|
|
7b75a32394 | ||
|
|
daf077b3c9 | ||
|
|
f6bbb59400 | ||
|
|
09813d5dbd | ||
|
|
fe509838f4 | ||
|
|
6a443d0074 | ||
|
|
8fc1065dd6 | ||
|
|
1af50ba0f5 | ||
|
|
82b3036b77 | ||
|
|
676412019c | ||
|
|
980f4e00e2 | ||
|
|
5731bf023a | ||
|
|
2511ca17aa | ||
|
|
fae653540b | ||
|
|
b0ca66cc1a | ||
|
|
a65e9c76bc | ||
|
|
adbac4c557 | ||
|
|
fc7b024e96 | ||
|
|
acee65ba25 | ||
|
|
244902ecfc | ||
|
|
4b1a678af2 | ||
|
|
59dd72b5c0 | ||
|
|
44c393f11a | ||
|
|
fddfbd8d2d | ||
|
|
e7e00bd428 | ||
|
|
07702e69ad | ||
|
|
e5c3757629 | ||
|
|
7031bbae43 | ||
|
|
4a6dfed676 | ||
|
|
bb0b414d71 | ||
|
|
95e25652c1 | ||
|
|
58155b0859 | ||
|
|
f579b79d2e | ||
|
|
47673be4e0 | ||
|
|
f67e6a9e9f | ||
|
|
110d8259fa | ||
|
|
8f253ffc43 | ||
|
|
6ca9cb6da1 | ||
|
|
1b63bdec12 | ||
|
|
bb52172516 | ||
|
|
1640495f34 | ||
|
|
64415a980f | ||
|
|
e06d141823 | ||
|
|
ac4d8679a1 | ||
|
|
8fc03a67b9 | ||
|
|
648506fe04 | ||
|
|
2c74ac8bfa | ||
|
|
963709c552 | ||
|
|
9af888a595 | ||
|
|
ec373b5b4d | ||
|
|
bfcd57881e | ||
|
|
b277a8c5e0 | ||
|
|
979a50716e | ||
|
|
c5f0da8151 | ||
|
|
aee0b5268f | ||
|
|
7e909f2bee | ||
|
|
584c90521a | ||
|
|
23ef8c78bd | ||
|
|
5ca025544e | ||
|
|
500ae0c72e | ||
|
|
c359207f1f | ||
|
|
159f2ebec0 | ||
|
|
a0db812606 | ||
|
|
d4c6a433d7 | ||
|
|
3a7cde9239 | ||
|
|
4b6c308ae9 | ||
|
|
002279f6a7 | ||
|
|
c807e52ad9 | ||
|
|
9557e3b910 | ||
|
|
ddc77884bd | ||
|
|
9d6337d5a8 | ||
|
|
117dd17215 | ||
|
|
f9eed0f6d0 | ||
|
|
4429145cdf | ||
|
|
5ea4cbf9ca | ||
|
|
c6473ca9e6 | ||
|
|
38b2a2f5b7 | ||
|
|
ebaa445bee | ||
|
|
8372c699f7 | ||
|
|
e1570e9512 | ||
|
|
dfb7304626 | ||
|
|
919531a82b | ||
|
|
81b2e9ccd2 | ||
|
|
4ef2aba4e2 | ||
|
|
4590655dc5 | ||
|
|
3040b70100 | ||
|
|
47b97aafc6 | ||
|
|
27e7383db6 | ||
|
|
42fe827cb3 | ||
|
|
3fa3e8357c | ||
|
|
6260607e1b | ||
|
|
3c1666e874 | ||
|
|
f4a082584c | ||
|
|
87d4dba32b | ||
|
|
329f68d167 | ||
|
|
2053cf085a | ||
|
|
16d48984c5 | ||
|
|
a17800283a | ||
|
|
9b1917cbdc | ||
|
|
e1e3d7a85b | ||
|
|
dc37d1f029 | ||
|
|
53e62f2be0 | ||
|
|
e6cc789c6f | ||
|
|
cfaef77b21 | ||
|
|
36fc9aa82a | ||
|
|
8d20669e46 | ||
|
|
9d8501cd64 | ||
|
|
88827e94f5 | ||
|
|
99d2a0c0b6 | ||
|
|
1d0582867b | ||
|
|
0ea6d9205d | ||
|
|
f438ef543b | ||
|
|
61cd9767c8 | ||
|
|
5fbf0a98b9 | ||
|
|
1671518ded | ||
|
|
f628ffca06 | ||
|
|
1d6f4fd4e7 | ||
|
|
9eedc0a36b | ||
|
|
a870fe9e1a | ||
|
|
d3f779cea9 | ||
|
|
fb4f41b996 | ||
|
|
11aac76fb6 | ||
|
|
98424f6cbb | ||
|
|
1cf7c59af9 | ||
|
|
13470fb0c3 | ||
|
|
3aa0fd1937 | ||
|
|
9e6f2336d1 | ||
|
|
8b8d62f598 | ||
|
|
4572ae5886 | ||
|
|
0802d4beb4 | ||
|
|
9361aa700a | ||
|
|
be5cad1cec | ||
|
|
fe20de2995 | ||
|
|
8714e4298e | ||
|
|
1baebe7475 | ||
|
|
6686ae43f3 | ||
|
|
cc2c0e9561 | ||
|
|
34d252a4bd | ||
|
|
025411c9fb | ||
|
|
4edb66d2b9 | ||
|
|
a17033dff4 | ||
|
|
04a5e56da7 | ||
|
|
0e6a3dd408 | ||
|
|
47f48a6a8c | ||
|
|
2ef7fabade | ||
|
|
5c2b475c01 | ||
|
|
559f4bc0d3 | ||
|
|
1357a4816b | ||
|
|
d1b8a56c0f | ||
|
|
7ea38298ea | ||
|
|
c08f1355db | ||
|
|
b6589637fa | ||
|
|
2ee2d2883a | ||
|
|
029c8ba917 | ||
|
|
60e1ee21ed | ||
|
|
a96e9158c4 | ||
|
|
e47765d7d5 | ||
|
|
e807435c8b | ||
|
|
14b41a93e2 | ||
|
|
f51fb9da29 | ||
|
|
df3ca3d3cc | ||
|
|
7786956b11 | ||
|
|
c17d62aeab | ||
|
|
65255121de | ||
|
|
b042945fef | ||
|
|
9d979217fa | ||
|
|
c177de2ec3 | ||
|
|
b4fd57d900 | ||
|
|
92339dfdcf | ||
|
|
8ae115028e | ||
|
|
2dd0221680 | ||
|
|
1a1923c6c0 | ||
|
|
5801ad4bdb | ||
|
|
388f2971e9 | ||
|
|
14c3a36ec0 | ||
|
|
1ad338ce31 | ||
|
|
71981e8a27 | ||
|
|
5cb10cd054 | ||
|
|
ecf576e9b9 | ||
|
|
09d17659b9 | ||
|
|
53673be5cb | ||
|
|
ed4a1d6ddd | ||
|
|
e3044b8b85 | ||
|
|
4ce05a064c | ||
|
|
2fbcc23451 | ||
|
|
3bdffed8c9 | ||
|
|
6cc8e87d46 | ||
|
|
7d8f549d97 | ||
|
|
fb2ef265bd | ||
|
|
96e8256781 | ||
|
|
dd40517f12 | ||
|
|
6c95b766d6 | ||
|
|
feffdcb71e | ||
|
|
f33e2c49ca | ||
|
|
1037acd4a2 | ||
|
|
68042fc755 | ||
|
|
1bdc77affe | ||
|
|
88f50da4fb | ||
|
|
bc1fbd9b6c | ||
|
|
c1b7b7c95e | ||
|
|
b0688eed5c | ||
|
|
e06ed03d33 | ||
|
|
e2a79394ab | ||
|
|
5db770ca44 | ||
|
|
df8aaa2005 | ||
|
|
882748f080 | ||
|
|
15035f4eb3 | ||
|
|
1d0a87f52a | ||
|
|
59b2cc5f79 | ||
|
|
d6758fc264 | ||
|
|
36418bec59 | ||
|
|
07bd8b2fa3 | ||
|
|
d989d02af9 | ||
|
|
503ce13122 | ||
|
|
8f77321adb | ||
|
|
60cdcea791 | ||
|
|
86cd4c5c30 | ||
|
|
62d5f61a0b | ||
|
|
25860867bb | ||
|
|
272860f071 | ||
|
|
767cfbc717 | ||
|
|
55af6ca84e | ||
|
|
88933ae051 | ||
|
|
4781beebee | ||
|
|
0049c74323 | ||
|
|
361727cec6 | ||
|
|
1b95177e0e | ||
|
|
8d34c54de2 | ||
|
|
2fa0eba3db | ||
|
|
6cc41e95c6 | ||
|
|
b1523f5b91 | ||
|
|
d16002546d | ||
|
|
186a93f5d1 | ||
|
|
3d4875bcfe | ||
|
|
441e30971a | ||
|
|
ff115c2349 | ||
|
|
f23e5bdb44 | ||
|
|
d0a232d86a | ||
|
|
9fef8386e6 | ||
|
|
b1680ba5c6 | ||
|
|
6cd59daf0a | ||
|
|
38f2b39ac4 | ||
|
|
51222738df | ||
|
|
b9835584d8 | ||
|
|
cff01021c2 | ||
|
|
d19b8a125c | ||
|
|
4caaa0033b | ||
|
|
f3a0a059ea | ||
|
|
290b0fe46f | ||
|
|
03a212eee4 | ||
|
|
f3b629bc06 | ||
|
|
b9e002f7b1 | ||
|
|
c90779beea | ||
|
|
bc8c8a049f | ||
|
|
1d9dc66265 | ||
|
|
886c149c3f | ||
|
|
369ca189d3 | ||
|
|
02c4bbe816 | ||
|
|
523c9f6576 | ||
|
|
c259430b09 | ||
|
|
fc94b90a03 | ||
|
|
6af521130d | ||
|
|
9c001e4f35 | ||
|
|
9e8dee36a6 | ||
|
|
26b17d8a3c | ||
|
|
1c5e2e3359 | ||
|
|
0437d37f23 | ||
|
|
ec6b1a44de | ||
|
|
08c661bb14 | ||
|
|
62f62d89c5 | ||
|
|
37dd8b40b2 | ||
|
|
15825f6c3f | ||
|
|
ad196bf03c | ||
|
|
332c4ca26e | ||
|
|
305edf1928 | ||
|
|
9c0c25ef99 | ||
|
|
458dae227f | ||
|
|
b1ba9fd54f | ||
|
|
dd42b5b851 | ||
|
|
d8e3edc729 | ||
|
|
36aa8623da | ||
|
|
7e24252447 | ||
|
|
164ce06177 | ||
|
|
c9d298c447 | ||
|
|
5129613ce8 | ||
|
|
b985ace7ed | ||
|
|
d28afac973 | ||
|
|
fd9b5ff7c4 | ||
|
|
75580bea27 | ||
|
|
db7056c53b | ||
|
|
b55181ffe6 | ||
|
|
81149e5aa8 | ||
|
|
3a341eee19 | ||
|
|
e19c7efbfe | ||
|
|
7e7c68321b | ||
|
|
9fa3f54c7c | ||
|
|
3ff273f1f2 | ||
|
|
e607b1962c | ||
|
|
2c4c6bf87c | ||
|
|
bf048e2a75 | ||
|
|
b0c4bb04e7 | ||
|
|
7e0e6c2786 | ||
|
|
d6a03df087 | ||
|
|
cfc89d2a74 | ||
|
|
c491c9dc8c | ||
|
|
eae066b3a2 | ||
|
|
71aa17bad6 | ||
|
|
93df01e266 | ||
|
|
8f96abb41e | ||
|
|
1457a6fe16 | ||
|
|
290c107698 | ||
|
|
bf7aaddbf9 | ||
|
|
59435e49c8 | ||
|
|
c3499e538e | ||
|
|
1d41b1c5a3 | ||
|
|
e303570b2f | ||
|
|
62940893f0 | ||
|
|
f8434bede5 | ||
|
|
c08e108fc3 | ||
|
|
cd9a160cae | ||
|
|
bba8b8be56 | ||
|
|
f56b5d58c6 | ||
|
|
ac4b0ed606 | ||
|
|
d3e71185e6 | ||
|
|
b4f2cd9ff4 | ||
|
|
fd8d305899 | ||
|
|
bde7ae944a | ||
|
|
99f83e5dc9 | ||
|
|
693aef5c04 | ||
|
|
b9ae537706 | ||
|
|
e41dd6d39d | ||
|
|
5d546f46e4 | ||
|
|
2bef5653b4 | ||
|
|
63d8549865 | ||
|
|
f6bac2f476 | ||
|
|
0dd51856d3 | ||
|
|
be01f2b511 | ||
|
|
045d2cf42f | ||
|
|
64ddd982fe | ||
|
|
b785b3f887 | ||
|
|
399421e20e | ||
|
|
a656d65d1d | ||
|
|
291a5d57c4 | ||
|
|
a9a91e3162 | ||
|
|
de4c6ab7b7 | ||
|
|
7ea9fc0c3b | ||
|
|
1965d5879f | ||
|
|
b2b907a86a | ||
|
|
e565de0724 | ||
|
|
7318e676f7 | ||
|
|
7c28d8ad51 | ||
|
|
3e21fb77c7 | ||
|
|
6b91e525db | ||
|
|
0aca03a919 | ||
|
|
e2ef8e2ef9 | ||
|
|
15c248184f | ||
|
|
a0c1b072b6 | ||
|
|
fe806cc4eb | ||
|
|
aacad78cdb | ||
|
|
b89f2dd862 | ||
|
|
36d01477cc | ||
|
|
e4090d00c9 | ||
|
|
ff55bc8209 | ||
|
|
b375c9efdc | ||
|
|
67b4fde5c3 | ||
|
|
63c6581d14 | ||
|
|
3ddd01981d | ||
|
|
bbd845905a | ||
|
|
3f53c37187 | ||
|
|
159c0d1104 | ||
|
|
82db08b76f | ||
|
|
83e84228f5 | ||
|
|
05edc715ef | ||
|
|
c503df5eec | ||
|
|
1f242473fe | ||
|
|
f7db5f8ae0 | ||
|
|
3b88d7cf94 | ||
|
|
cac82f2eba | ||
|
|
5811b469cf | ||
|
|
af1175f32e | ||
|
|
7a5ce5761f | ||
|
|
34104355cb | ||
|
|
143c1255d8 | ||
|
|
a9d0e5ac81 | ||
|
|
c8b3ee51ed | ||
|
|
c964067139 | ||
|
|
539f590c4c | ||
|
|
71dddd4a1b | ||
|
|
792f5dd7b5 | ||
|
|
a3af49d92a | ||
|
|
4289b43a81 | ||
|
|
7f55623acf | ||
|
|
52060b65be | ||
|
|
fd826749e4 | ||
|
|
627e15c3dd | ||
|
|
90f6890180 | ||
|
|
61eb397d2b | ||
|
|
3a5e5364c7 | ||
|
|
25779d04a6 | ||
|
|
242900e87f | ||
|
|
05f07d1788 | ||
|
|
f961f4ccac | ||
|
|
145377b05f | ||
|
|
bc88887195 | ||
|
|
5362b1c21c | ||
|
|
0cfd3265ba | ||
|
|
1099128513 | ||
|
|
ad50c81a6b | ||
|
|
0817f113c6 | ||
|
|
4a9a07a9ef | ||
|
|
61f50cfe60 | ||
|
|
92888778c2 | ||
|
|
987f9b9dba | ||
|
|
97d95f37cc | ||
|
|
836cd04564 | ||
|
|
c26f54161d | ||
|
|
b540009ce6 | ||
|
|
e58e209950 | ||
|
|
4597a23104 | ||
|
|
3aacf4bcd2 | ||
|
|
6e6b663fac | ||
|
|
6dad7eafcf | ||
|
|
5a38143987 | ||
|
|
4d6d31d624 | ||
|
|
938c82be3f | ||
|
|
dc2e249566 | ||
|
|
bb8fdcabcb | ||
|
|
6cf4dbc78c | ||
|
|
8cd0ac5451 | ||
|
|
33745f0b0c | ||
|
|
29ffed219f | ||
|
|
9629d0f715 | ||
|
|
01651984d7 | ||
|
|
c7c15250ca | ||
|
|
36b96eafcc | ||
|
|
94f930ee22 | ||
|
|
3f740d2904 | ||
|
|
f8529adfcf | ||
|
|
77ccbdd322 | ||
|
|
f2846efd2c | ||
|
|
131f9c4bc9 | ||
|
|
d7c06fff50 | ||
|
|
09a22d9dc4 | ||
|
|
0fbab04253 | ||
|
|
c963e99dca | ||
|
|
7a555d127f | ||
|
|
866408f673 | ||
|
|
a7e5ab1a6a | ||
|
|
14d16d61e6 | ||
|
|
b988e4a813 | ||
|
|
ae33c8db1b | ||
|
|
9f1c5ac6bb | ||
|
|
448e7d0739 | ||
|
|
8971ff9057 | ||
|
|
2d6b16b2ce | ||
|
|
44ab1643fa | ||
|
|
a64bffd83a | ||
|
|
534c5c3c64 | ||
|
|
99d3f9918f | ||
|
|
aaebf029db | ||
|
|
e2c2ace0e3 | ||
|
|
c76002663f | ||
|
|
c5317370c8 | ||
|
|
4b09f4a654 | ||
|
|
0c57113d8e | ||
|
|
d7dd77a5af | ||
|
|
5c5b88ebcc | ||
|
|
8df0248d4f | ||
|
|
58e48fdf14 | ||
|
|
407fc56218 | ||
|
|
398527d3f1 | ||
|
|
59745a695c | ||
|
|
2aaeda6ca8 | ||
|
|
5c78de2f46 | ||
|
|
93efc21452 | ||
|
|
50ad005e7c | ||
|
|
71c3bcdd29 | ||
|
|
e5bf04a407 | ||
|
|
fe8b2cb761 | ||
|
|
7c37f929a5 | ||
|
|
1b82d10b39 | ||
|
|
6b6e2490e7 | ||
|
|
ebee54cf92 | ||
|
|
b7acfb0dcc | ||
|
|
65dab45582 | ||
|
|
43086f9582 | ||
|
|
ed1aa74aff | ||
|
|
3ba128793a | ||
|
|
ffbbdc1576 | ||
|
|
6ff55cfff7 | ||
|
|
e9f1f781e1 | ||
|
|
6da36fe098 | ||
|
|
acb6510312 | ||
|
|
13248506c5 | ||
|
|
fb9740f5c3 | ||
|
|
61bf788f52 | ||
|
|
fd116e0178 | ||
|
|
661d981231 | ||
|
|
6a701591af | ||
|
|
02431c6ef4 | ||
|
|
802b179880 | ||
|
|
6c2104b84b | ||
|
|
a3f432dc88 | ||
|
|
7f4caedf40 | ||
|
|
eebd06f0d8 | ||
|
|
0ea66b6bb0 | ||
|
|
084cdd7200 | ||
|
|
2eff9e0230 | ||
|
|
6615bc4a2a | ||
|
|
387f18be98 | ||
|
|
3b5a3eccfe | ||
|
|
36083a8bd9 | ||
|
|
7e16825bf4 | ||
|
|
9ba2724d0a | ||
|
|
7866851f5d | ||
|
|
c938035ec1 | ||
|
|
965fdc5e9b | ||
|
|
eab932b4a0 | ||
|
|
27a24262c8 | ||
|
|
31584de225 | ||
|
|
6b2f90019a | ||
|
|
208147db9e | ||
|
|
e4f70fa4fe | ||
|
|
4d09abd0d3 | ||
|
|
1304f4dc39 | ||
|
|
ac027d9267 | ||
|
|
0b6d343616 | ||
|
|
92e8f125f9 | ||
|
|
bef15482af | ||
|
|
93d78b3b2e | ||
|
|
d38b7deeeb | ||
|
|
b656e1dd0a | ||
|
|
f1cec895b9 | ||
|
|
9bf6922d97 | ||
|
|
41fc4096e4 | ||
|
|
e46564cb7e | ||
|
|
77751c1d28 | ||
|
|
054a1e4017 | ||
|
|
da27d74111 | ||
|
|
970278228d | ||
|
|
ee3f2d62cf | ||
|
|
ec6d5031cf | ||
|
|
80f338c3af | ||
|
|
268c9a1c26 | ||
|
|
e8e01b5965 | ||
|
|
23a042b667 | ||
|
|
c2c1537858 | ||
|
|
65d5f4c426 | ||
|
|
a1eb33b1f6 | ||
|
|
9eadd92d05 | ||
|
|
b0e1294584 | ||
|
|
f1fd29a477 | ||
|
|
fc9a6b98d1 | ||
|
|
305f6c610c | ||
|
|
66f4732db5 | ||
|
|
ccdfa546b4 | ||
|
|
855e194baa | ||
|
|
e0c06615fb | ||
|
|
03a4809866 | ||
|
|
0aa7bd6a4e | ||
|
|
78b530f8b8 | ||
|
|
ace47c61b1 | ||
|
|
29796f51d7 | ||
|
|
4d2ce7a2be | ||
|
|
8dc45263cd | ||
|
|
d8d95d8efe | ||
|
|
f081591354 | ||
|
|
35e96fecdb | ||
|
|
841fbfa7ee | ||
|
|
89397ae7cc | ||
|
|
6c57c2ac2a | ||
|
|
40663eb52f | ||
|
|
90c9cc17b9 | ||
|
|
125c4f43cf | ||
|
|
634e4abcc1 | ||
|
|
a5431330d1 | ||
|
|
30fc6d94c5 | ||
|
|
694d8f1984 | ||
|
|
bff8fc8230 | ||
|
|
5f7414e84c | ||
|
|
8c707555f2 | ||
|
|
63ce2de3bf | ||
|
|
5e86cca277 | ||
|
|
4bbed2601c | ||
|
|
e0bda8cf53 | ||
|
|
a1c807d65b | ||
|
|
77f5c290cc | ||
|
|
4294b446f3 | ||
|
|
664e8e5526 | ||
|
|
264ade3db9 | ||
|
|
1a6e5e9e2b | ||
|
|
6aa4bb549a | ||
|
|
dd76909f02 | ||
|
|
a454adece8 | ||
|
|
f2adc4d283 | ||
|
|
6af9835f74 | ||
|
|
b823baa387 | ||
|
|
6791d2d46e | ||
|
|
3c343af562 | ||
|
|
fe9ed4c5f7 | ||
|
|
7374e7ee23 | ||
|
|
3c9c0e244a | ||
|
|
64c219b02d | ||
|
|
7e68f8faf2 | ||
|
|
c868098042 | ||
|
|
b3c0cda2be | ||
|
|
2176d1f3df | ||
|
|
979b9859af | ||
|
|
a9ce4a1aed | ||
|
|
a01fb7ff1c | ||
|
|
0e631508b2 | ||
|
|
eb9915d445 | ||
|
|
eedf7d4280 | ||
|
|
aaca487b8f | ||
|
|
a7d6c0f25c | ||
|
|
158a250357 | ||
|
|
b9d7d19dea | ||
|
|
a837f86999 | ||
|
|
a0e4b1aaf9 | ||
|
|
4d10be2aa5 |
@@ -1,5 +1,20 @@
|
||||
root = true
|
||||
|
||||
[*.kt]
|
||||
[*.{kt,kts}]
|
||||
indent_size = 2
|
||||
twitter_compose_allowed_composition_locals=LocalExtendedColors
|
||||
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
|
||||
|
||||
# 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 -->
|
||||
|
||||
8
.github/workflows/android.yml
vendored
8
.github/workflows/android.yml
vendored
@@ -18,19 +18,21 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: set up JDK 11
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew qa --parallel
|
||||
run: ./gradlew qa
|
||||
|
||||
- name: Archive reports for failed build
|
||||
if: ${{ failure() }}
|
||||
|
||||
84
.github/workflows/diffuse.yml
vendored
Normal file
84
.github/workflows/diffuse.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: APK Diff
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
pull-requests: write # to comment on PR
|
||||
|
||||
jobs:
|
||||
assemble-base:
|
||||
if: ${{ github.repository != 'signalapp/Signal-Android' }}
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Cache base apk
|
||||
id: cache-base
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: diffuse-base.apk
|
||||
key: diffuse-${{ github.event.pull_request.base.sha }}
|
||||
|
||||
|
||||
- name: Build with Gradle
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
run: ./gradlew assemblePlayProdRelease
|
||||
|
||||
- name: Copy base apk
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-base.apk
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
clean: 'false'
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assemblePlayProdRelease
|
||||
|
||||
- name: Copy PR apk
|
||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk
|
||||
|
||||
- id: diffuse
|
||||
uses: usefulness/diffuse-action@v1
|
||||
with:
|
||||
old-file-path: diffuse-base.apk
|
||||
new-file-path: diffuse-new.apk
|
||||
|
||||
- uses: peter-evans/find-comment@v2
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: Diffuse output
|
||||
|
||||
- uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
body: |
|
||||
Diffuse output:
|
||||
|
||||
${{ steps.diffuse.outputs.diff-gh-comment }}
|
||||
edit-mode: replace
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: diffuse-output
|
||||
path: ${{ steps.diffuse.outputs.diff-file }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,6 +3,7 @@ captures/
|
||||
project.properties
|
||||
keystore.debug.properties
|
||||
keystore.staging.properties
|
||||
nightly-url.txt
|
||||
.project
|
||||
.settings
|
||||
bin/
|
||||
@@ -28,3 +29,4 @@ jni/libspeex/.deps/
|
||||
pkcs11.password
|
||||
dev.keystore
|
||||
maps.key
|
||||
/local/
|
||||
|
||||
7
.idea/codeStyles/Project.xml
generated
7
.idea/codeStyles/Project.xml
generated
@@ -212,5 +212,12 @@
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
6
.idea/copyright/Signal.xml
generated
Normal file
6
.idea/copyright/Signal.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Copyright &#36;today.year Signal Messenger, LLC SPDX-License-Identifier: AGPL-3.0-only" />
|
||||
<option name="myName" value="Signal" />
|
||||
</copyright>
|
||||
</component>
|
||||
7
.idea/copyright/profiles_settings.xml
generated
Normal file
7
.idea/copyright/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings>
|
||||
<module2copyright>
|
||||
<element module="All" copyright="Signal" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
||||
9
.idea/fileTemplates/internal/AnnotationType.java
generated
Normal file
9
.idea/fileTemplates/internal/AnnotationType.java
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public @interface ${NAME} {
|
||||
}
|
||||
9
.idea/fileTemplates/internal/Class.java
generated
Normal file
9
.idea/fileTemplates/internal/Class.java
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public class ${NAME} {
|
||||
}
|
||||
9
.idea/fileTemplates/internal/Enum.java
generated
Normal file
9
.idea/fileTemplates/internal/Enum.java
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public enum ${NAME} {
|
||||
}
|
||||
9
.idea/fileTemplates/internal/Interface.java
generated
Normal file
9
.idea/fileTemplates/internal/Interface.java
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public interface ${NAME} {
|
||||
}
|
||||
11
.idea/fileTemplates/internal/Kotlin Class.kt
generated
Normal file
11
.idea/fileTemplates/internal/Kotlin Class.kt
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
class ${NAME} {
|
||||
}
|
||||
11
.idea/fileTemplates/internal/Kotlin Enum.kt
generated
Normal file
11
.idea/fileTemplates/internal/Kotlin Enum.kt
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
enum class ${NAME} {
|
||||
}
|
||||
9
.idea/fileTemplates/internal/Kotlin File.kt
generated
Normal file
9
.idea/fileTemplates/internal/Kotlin File.kt
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
11
.idea/fileTemplates/internal/Kotlin Interface.kt
generated
Normal file
11
.idea/fileTemplates/internal/Kotlin Interface.kt
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
interface ${NAME} {
|
||||
}
|
||||
@@ -15,6 +15,12 @@ Truths which we believe to be self-evident:
|
||||
1. **There is no such thing as time.** Protocol ideas that require synchronized clocks are doomed to failure.
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
1. You'll need to get the `libwebp` submodule after checking out the repository with `git submodule init && git submodule update`
|
||||
1. Most things are pretty straightforward, and opening the project in Android Studio should get you most of the way there.
|
||||
1. Depending on your configuration, you'll also likely need to install additional SDK Tool components, namely the versions of NDK and CMake we are currently using in our [Docker](https://github.com/signalapp/Signal-Android/blob/main/reproducible-builds/Dockerfile#L30) configuration.
|
||||
|
||||
## Issues
|
||||
|
||||
### Useful bug reports
|
||||
|
||||
152
LICENSE
152
LICENSE
@@ -1,23 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -619,3 +617,45 @@ Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Signal Android
|
||||
# Signal Android
|
||||
|
||||
Signal is a simple, powerful, and secure messenger.
|
||||
|
||||
@@ -54,8 +54,8 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013-2022 Signal
|
||||
Copyright 2013-2024 Signal Messenger, LLC
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
|
||||
2
apntool/.gitignore
vendored
2
apntool/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*.db
|
||||
*.db.gz
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,106 +0,0 @@
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
import sqlite3
|
||||
import gzip
|
||||
from progressbar import ProgressBar, Counter, Timer
|
||||
from lxml import etree
|
||||
|
||||
parser = argparse.ArgumentParser(prog='apntool', description="""Process Android's apn xml files and drop them into an
|
||||
easily queryable SQLite db. Tested up to version 9 of
|
||||
their APN file.""")
|
||||
parser.add_argument('-v', '--version', action='version', version='%(prog)s v1.1')
|
||||
parser.add_argument('-i', '--input', help='the xml file to parse', default='apns.xml', required=False)
|
||||
parser.add_argument('-o', '--output', help='the sqlite db output file', default='apns.db', required=False)
|
||||
parser.add_argument('--quiet', help='do not show progress or verbose instructions', action='store_true', required=False)
|
||||
parser.add_argument('--no-gzip', help="do not gzip after creation", action='store_true', required=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def normalized(target):
|
||||
o2_typo = re.compile(r"02\.co\.uk")
|
||||
port_typo = re.compile(r"(\d+\.\d+\.\d+\.\d+)\.(\d+)")
|
||||
leading_zeros = re.compile(r"(/|\.|^)0+(\d+)")
|
||||
subbed = o2_typo.sub(r'o2.co.uk', target)
|
||||
subbed = port_typo.sub(r'\1:\2', subbed)
|
||||
subbed = leading_zeros.sub(r'\1\2', subbed)
|
||||
return subbed
|
||||
|
||||
try:
|
||||
connection = sqlite3.connect(args.output)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT SQLITE_VERSION()')
|
||||
version = cursor.fetchone()
|
||||
if not args.quiet:
|
||||
print("SQLite version: %s" % version)
|
||||
print("Opening %s" % args.input)
|
||||
|
||||
cursor.execute("PRAGMA legacy_file_format=ON")
|
||||
cursor.execute("PRAGMA journal_mode=DELETE")
|
||||
cursor.execute("PRAGMA page_size=32768")
|
||||
cursor.execute("VACUUM")
|
||||
cursor.execute("DROP TABLE IF EXISTS apns")
|
||||
cursor.execute("""CREATE TABLE apns(_id INTEGER PRIMARY KEY, mccmnc TEXT, mcc TEXT, mnc TEXT, carrier TEXT,
|
||||
apn TEXT, mmsc TEXT, port INTEGER, type TEXT, protocol TEXT, bearer TEXT, roaming_protocol TEXT,
|
||||
carrier_enabled INTEGER, mmsproxy TEXT, mmsport INTEGER, proxy TEXT, mvno_match_data TEXT,
|
||||
mvno_type TEXT, authtype INTEGER, user TEXT, password TEXT, server TEXT)""")
|
||||
|
||||
apns = etree.parse(args.input)
|
||||
root = apns.getroot()
|
||||
pbar = None
|
||||
if not args.quiet:
|
||||
pbar = ProgressBar(widgets=['Processed: ', Counter(), ' apns (', Timer(), ')'], maxval=len(list(root))).start()
|
||||
|
||||
count = 0
|
||||
for apn in root.iter("apn"):
|
||||
if apn.get("mmsc") is None:
|
||||
continue
|
||||
sqlvars = ["?" for x in apn.attrib.keys()] + ["?"]
|
||||
mccmnc = "%s%s" % (apn.get("mcc"), apn.get("mnc"))
|
||||
normalized_mmsc = normalized(apn.get("mmsc"))
|
||||
if normalized_mmsc != apn.get("mmsc"):
|
||||
print("normalize MMSC: %s => %s" % (apn.get("mmsc"), normalized_mmsc))
|
||||
apn.set("mmsc", normalized_mmsc)
|
||||
|
||||
if not apn.get("mmsproxy") is None:
|
||||
normalized_mmsproxy = normalized(apn.get("mmsproxy"))
|
||||
if normalized_mmsproxy != apn.get("mmsproxy"):
|
||||
print("normalize proxy: %s => %s" % (apn.get("mmsproxy"), normalized_mmsproxy))
|
||||
apn.set("mmsproxy", normalized_mmsproxy)
|
||||
|
||||
values = [apn.get(attrib) for attrib in apn.attrib.keys()] + [mccmnc]
|
||||
keys = apn.attrib.keys() + ["mccmnc"]
|
||||
|
||||
cursor.execute("SELECT 1 FROM apns WHERE mccmnc = ? AND apn = ?", [mccmnc, apn.get("apn")])
|
||||
if cursor.fetchone() is None:
|
||||
statement = "INSERT INTO apns (%s) VALUES (%s)" % (", ".join(keys), ", ".join(sqlvars))
|
||||
cursor.execute(statement, values)
|
||||
|
||||
count += 1
|
||||
if not args.quiet:
|
||||
pbar.update(count)
|
||||
|
||||
if not args.quiet:
|
||||
pbar.finish()
|
||||
connection.commit()
|
||||
print("Successfully written to %s" % args.output)
|
||||
|
||||
if not args.no_gzip:
|
||||
gzipped_file = "%s.gz" % (args.output,)
|
||||
with open(args.output, 'rb') as orig:
|
||||
with gzip.open(gzipped_file, 'wb') as gzipped:
|
||||
gzipped.writelines(orig)
|
||||
print("Successfully gzipped to %s" % gzipped_file)
|
||||
|
||||
if not args.quiet:
|
||||
print("\nTo include this in the distribution, copy it to the project's assets/databases/ directory.")
|
||||
print("If you support API 10 or lower, you must use the gzipped version to avoid corruption.")
|
||||
|
||||
except sqlite3.Error as e:
|
||||
if connection:
|
||||
connection.rollback()
|
||||
print("Error: %s" % e.args[0])
|
||||
sys.exit(1)
|
||||
finally:
|
||||
if connection:
|
||||
connection.close()
|
||||
@@ -1,3 +0,0 @@
|
||||
argparse>=1.2.1
|
||||
lxml>=3.3.3
|
||||
progressbar-latest>=2.4
|
||||
705
app/build.gradle
705
app/build.gradle
@@ -1,705 +0,0 @@
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'com.google.protobuf'
|
||||
id 'androidx.navigation.safeargs'
|
||||
id 'org.jlleitschuh.gradle.ktlint'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'app.cash.exhaustive'
|
||||
id 'kotlin-parcelize'
|
||||
id 'com.squareup.wire'
|
||||
id 'android-constants'
|
||||
id 'translations'
|
||||
}
|
||||
|
||||
apply from: 'static-ips.gradle'
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = 'com.google.protobuf:protoc:3.18.0'
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().each { task ->
|
||||
task.builtins {
|
||||
java {
|
||||
option "lite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wire {
|
||||
kotlin {
|
||||
javaInterop = true
|
||||
}
|
||||
|
||||
sourcePath {
|
||||
srcDir 'src/main/protowire'
|
||||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
|
||||
version = "0.47.1"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1250
|
||||
def canonicalVersionName = "6.19.2"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
'armeabi-v7a' : 1,
|
||||
'arm64-v8a' : 2,
|
||||
'x86' : 3,
|
||||
'x86_64' : 4]
|
||||
|
||||
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
|
||||
|
||||
def selectableVariants = [
|
||||
'nightlyProdSpinner',
|
||||
'nightlyProdPerf',
|
||||
'nightlyProdRelease',
|
||||
'nightlyStagingRelease',
|
||||
'nightlyPnpPerf',
|
||||
'nightlyPnpRelease',
|
||||
'playProdDebug',
|
||||
'playProdSpinner',
|
||||
'playProdCanary',
|
||||
'playProdPerf',
|
||||
'playProdBenchmark',
|
||||
'playProdInstrumentation',
|
||||
'playProdRelease',
|
||||
'playStagingDebug',
|
||||
'playStagingCanary',
|
||||
'playStagingSpinner',
|
||||
'playStagingPerf',
|
||||
'playStagingInstrumentation',
|
||||
'playPnpDebug',
|
||||
'playPnpSpinner',
|
||||
'playStagingRelease',
|
||||
'websiteProdSpinner',
|
||||
'websiteProdRelease',
|
||||
]
|
||||
|
||||
android {
|
||||
namespace 'org.thoughtcrime.securesms'
|
||||
|
||||
buildToolsVersion = signalBuildToolsVersion
|
||||
compileSdkVersion = signalCompileSdkVersion
|
||||
|
||||
flavorDimensions 'distribution', 'environment'
|
||||
useLibrary 'org.apache.http.legacy'
|
||||
testBuildType 'instrumentation'
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
freeCompilerArgs = ["-Xallow-result-return-type"]
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (keystores.debug != null) {
|
||||
debug {
|
||||
storeFile file("${project.rootDir}/${keystores.debug.storeFile}")
|
||||
storePassword keystores.debug.storePassword
|
||||
keyAlias keystores.debug.keyAlias
|
||||
keyPassword keystores.debug.keyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
|
||||
unitTests {
|
||||
includeAndroidResources = true
|
||||
}
|
||||
|
||||
managedDevices {
|
||||
devices {
|
||||
pixel3api30 (ManagedVirtualDevice) {
|
||||
device = "Pixel 3"
|
||||
apiLevel = 30
|
||||
systemImageSource = "google-atd"
|
||||
require64Bit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
sourceSets {
|
||||
test {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
|
||||
androidTest {
|
||||
java.srcDirs += "$projectDir/src/testShared"
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility signalJavaVersion
|
||||
targetCompatibility signalJavaVersion
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += ['LICENSE.txt', 'LICENSE', 'NOTICE', 'asm-license.txt', 'META-INF/LICENSE', 'META-INF/LICENSE.md', 'META-INF/NOTICE', 'META-INF/LICENSE-notice.md', 'META-INF/proguard/androidx-annotations.pro', 'libsignal_jni.dylib', 'signal_jni.dll']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
compose true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = '1.3.2'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode canonicalVersionCode * postFixSize
|
||||
versionName canonicalVersionName
|
||||
|
||||
minSdkVersion signalMinSdkVersion
|
||||
targetSdkVersion signalTargetSdkVersion
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
project.ext.set("archivesBaseName", "Signal")
|
||||
|
||||
manifestPlaceholders = [mapsKey:"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"]
|
||||
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
|
||||
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.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_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\""
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}"
|
||||
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\", \"https://sfu.staging.test.voip.signal.org\"}"
|
||||
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
|
||||
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
|
||||
buildConfigField "String[]", "SIGNAL_SERVICE_IPS", service_ips
|
||||
buildConfigField "String[]", "SIGNAL_STORAGE_IPS", storage_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDN_IPS", cdn_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDN2_IPS", cdn2_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDS_IPS", cds_ips
|
||||
buildConfigField "String[]", "SIGNAL_KBS_IPS", kbs_ips
|
||||
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
|
||||
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
|
||||
"\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
|
||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
|
||||
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\""
|
||||
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
|
||||
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
|
||||
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
|
||||
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
|
||||
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\""
|
||||
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
|
||||
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
|
||||
buildConfigField "String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\""
|
||||
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\""
|
||||
buildConfigField "boolean", "TRACING_ENABLED", "false"
|
||||
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
|
||||
resConfigs autoResConfig()
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable !project.hasProperty('generateBaselineProfile')
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
|
||||
testInstrumentationRunner "org.thoughtcrime.securesms.testing.SignalTestRunner"
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
if (keystores['debug'] != null) {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
isDefault true
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'),
|
||||
'proguard/proguard-firebase-messaging.pro',
|
||||
'proguard/proguard-google-play-services.pro',
|
||||
'proguard/proguard-jackson.pro',
|
||||
'proguard/proguard-sqlite.pro',
|
||||
'proguard/proguard-appcompat-v7.pro',
|
||||
'proguard/proguard-square-okhttp.pro',
|
||||
'proguard/proguard-square-okio.pro',
|
||||
'proguard/proguard-rounded-image-view.pro',
|
||||
'proguard/proguard-glide.pro',
|
||||
'proguard/proguard-shortcutbadger.pro',
|
||||
'proguard/proguard-retrofit.pro',
|
||||
'proguard/proguard-webrtc.pro',
|
||||
'proguard/proguard-klinker.pro',
|
||||
'proguard/proguard-mobilecoin.pro',
|
||||
'proguard/proguard-retrolambda.pro',
|
||||
'proguard/proguard-okhttp.pro',
|
||||
'proguard/proguard-ez-vcard.pro',
|
||||
'proguard/proguard.cfg'
|
||||
testProguardFiles 'proguard/proguard-automation.pro',
|
||||
'proguard/proguard.cfg'
|
||||
|
||||
manifestPlaceholders = [mapsKey:getMapsKey()]
|
||||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
|
||||
}
|
||||
|
||||
instrumentation {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
applicationIdSuffix ".instrumentation"
|
||||
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Instrumentation\""
|
||||
}
|
||||
|
||||
spinner {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Spinner\""
|
||||
}
|
||||
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles = buildTypes.debug.proguardFiles
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Release\""
|
||||
}
|
||||
|
||||
perf {
|
||||
initWith debug
|
||||
isDefault false
|
||||
debuggable false
|
||||
minifyEnabled true
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
|
||||
buildConfigField "boolean", "TRACING_ENABLED", "true"
|
||||
}
|
||||
|
||||
benchmark {
|
||||
initWith debug
|
||||
isDefault false
|
||||
debuggable false
|
||||
minifyEnabled true
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Benchmark\""
|
||||
buildConfigField "boolean", "TRACING_ENABLED", "true"
|
||||
}
|
||||
|
||||
canary {
|
||||
initWith debug
|
||||
isDefault false
|
||||
minifyEnabled false
|
||||
matchingFallbacks = ['debug']
|
||||
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Canary\""
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
play {
|
||||
dimension 'distribution'
|
||||
isDefault true
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"play\""
|
||||
}
|
||||
|
||||
website {
|
||||
dimension 'distribution'
|
||||
ext.websiteUpdateUrl = "https://updates.signal.org/android"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
|
||||
}
|
||||
|
||||
nightly {
|
||||
dimension 'distribution'
|
||||
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
|
||||
ext.websiteUpdateUrl = "null"
|
||||
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
|
||||
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
|
||||
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\""
|
||||
}
|
||||
|
||||
prod {
|
||||
dimension 'environment'
|
||||
|
||||
isDefault true
|
||||
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\""
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Prod\""
|
||||
}
|
||||
|
||||
staging {
|
||||
dimension 'environment'
|
||||
|
||||
applicationIdSuffix ".staging"
|
||||
|
||||
buildConfigField "String", "SIGNAL_URL", "\"https://chat.staging.signal.org\""
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.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 "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
|
||||
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
|
||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
|
||||
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
|
||||
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
|
||||
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
|
||||
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\""
|
||||
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
|
||||
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 "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
|
||||
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\""
|
||||
}
|
||||
|
||||
pnp {
|
||||
dimension 'environment'
|
||||
|
||||
initWith staging
|
||||
applicationIdSuffix ".pnp"
|
||||
|
||||
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Pnp\""
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError true
|
||||
baseline file('lint-baseline.xml')
|
||||
checkReleaseBuilds false
|
||||
disable 'LintError'
|
||||
}
|
||||
|
||||
android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
if (output.baseName.contains('nightly')) {
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
|
||||
def tag = getCurrentGitTag()
|
||||
if (tag != null && tag.length() > 0) {
|
||||
if (tag.startsWith("v")) {
|
||||
tag = tag.substring(1)
|
||||
}
|
||||
output.versionNameOverride = tag
|
||||
}
|
||||
} else {
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
|
||||
def abiName = output.getFilter("ABI") ?: 'universal'
|
||||
def postFix = abiPostFix.get(abiName, 0)
|
||||
|
||||
if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
|
||||
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android.variantFilter { variant ->
|
||||
def distribution = variant.getFlavors().get(0).name
|
||||
def environment = variant.getFlavors().get(1).name
|
||||
def buildType = variant.buildType.name
|
||||
def fullName = distribution + environment.capitalize() + buildType.capitalize()
|
||||
|
||||
if (!selectableVariants.contains(fullName)) {
|
||||
variant.setIgnore(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.androidx.fragment.ktx
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
coreLibraryDesugaring libs.android.tools.desugar
|
||||
|
||||
implementation (libs.androidx.appcompat) {
|
||||
version {
|
||||
strictly '1.6.1'
|
||||
}
|
||||
}
|
||||
implementation libs.androidx.window.window
|
||||
implementation libs.androidx.window.java
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.material.material
|
||||
implementation libs.androidx.legacy.support
|
||||
implementation libs.androidx.preference
|
||||
implementation libs.androidx.legacy.preference
|
||||
implementation libs.androidx.gridlayout
|
||||
implementation libs.androidx.exifinterface
|
||||
implementation libs.androidx.constraintlayout
|
||||
implementation libs.androidx.multidex
|
||||
implementation libs.androidx.navigation.fragment.ktx
|
||||
implementation libs.androidx.navigation.ui.ktx
|
||||
implementation libs.androidx.lifecycle.viewmodel.ktx
|
||||
implementation libs.androidx.lifecycle.livedata.ktx
|
||||
implementation libs.androidx.lifecycle.process
|
||||
implementation libs.androidx.lifecycle.viewmodel.savedstate
|
||||
implementation libs.androidx.lifecycle.common.java8
|
||||
implementation libs.androidx.lifecycle.reactivestreams.ktx
|
||||
implementation libs.androidx.camera.core
|
||||
implementation libs.androidx.camera.camera2
|
||||
implementation libs.androidx.camera.lifecycle
|
||||
implementation libs.androidx.camera.view
|
||||
implementation libs.androidx.concurrent.futures
|
||||
implementation libs.androidx.autofill
|
||||
implementation libs.androidx.biometric
|
||||
implementation libs.androidx.sharetarget
|
||||
implementation libs.androidx.profileinstaller
|
||||
implementation libs.androidx.asynclayoutinflater
|
||||
implementation libs.androidx.asynclayoutinflater.appcompat
|
||||
|
||||
implementation (libs.firebase.messaging) {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
|
||||
implementation libs.google.play.services.maps
|
||||
implementation libs.google.play.services.auth
|
||||
|
||||
implementation libs.bundles.exoplayer
|
||||
|
||||
implementation libs.conscrypt.android
|
||||
implementation libs.signal.aesgcmprovider
|
||||
|
||||
implementation project(':libsignal-service')
|
||||
implementation project(':paging')
|
||||
implementation project(':core-util')
|
||||
implementation project(':glide-config')
|
||||
implementation project(':video')
|
||||
implementation project(':device-transfer')
|
||||
implementation project(':image-editor')
|
||||
implementation project(':donations')
|
||||
implementation project(':contacts')
|
||||
implementation project(':qr')
|
||||
implementation project(':sms-exporter')
|
||||
implementation project(':sticky-header-grid')
|
||||
implementation project(':photoview')
|
||||
|
||||
implementation libs.libsignal.android
|
||||
implementation libs.google.protobuf.javalite
|
||||
|
||||
implementation(libs.mobilecoin) {
|
||||
exclude group: 'com.google.protobuf'
|
||||
}
|
||||
|
||||
implementation(libs.signal.argon2) {
|
||||
artifact {
|
||||
type = "aar"
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.signal.ringrtc
|
||||
|
||||
implementation libs.leolin.shortcutbadger
|
||||
implementation libs.emilsjolander.stickylistheaders
|
||||
implementation libs.jpardogo.materialtabstrip
|
||||
implementation libs.apache.httpclient.android
|
||||
implementation libs.glide.glide
|
||||
implementation libs.roundedimageview
|
||||
implementation libs.materialish.progress
|
||||
implementation libs.greenrobot.eventbus
|
||||
implementation libs.waitingdots
|
||||
implementation libs.google.zxing.android.integration
|
||||
implementation libs.google.zxing.core
|
||||
implementation libs.google.flexbox
|
||||
implementation (libs.subsampling.scale.image.view) {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
}
|
||||
implementation (libs.android.tooltips) {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
}
|
||||
implementation (libs.android.smsmms) {
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp'
|
||||
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
|
||||
}
|
||||
implementation libs.stream
|
||||
|
||||
implementation libs.lottie
|
||||
|
||||
implementation libs.signal.android.database.sqlcipher
|
||||
implementation libs.androidx.sqlite
|
||||
|
||||
implementation (libs.google.ez.vcard) {
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
exclude group: 'org.freemarker'
|
||||
}
|
||||
implementation libs.dnsjava
|
||||
implementation libs.kotlinx.collections.immutable
|
||||
|
||||
spinnerImplementation project(":spinner")
|
||||
|
||||
canaryImplementation libs.square.leakcanary
|
||||
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
testImplementation testLibs.mockito.core
|
||||
testImplementation testLibs.mockito.kotlin
|
||||
|
||||
testImplementation testLibs.androidx.test.core
|
||||
testImplementation (testLibs.robolectric.robolectric) {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation testLibs.robolectric.shadows.multidex
|
||||
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) {
|
||||
force = true
|
||||
}
|
||||
testImplementation testLibs.hamcrest.hamcrest
|
||||
testImplementation testLibs.mockk
|
||||
|
||||
testImplementation(testFixtures(project(":libsignal-service")))
|
||||
|
||||
androidTestImplementation testLibs.androidx.test.ext.junit
|
||||
androidTestImplementation testLibs.espresso.core
|
||||
androidTestImplementation testLibs.androidx.test.core
|
||||
androidTestImplementation testLibs.androidx.test.core.ktx
|
||||
androidTestImplementation testLibs.androidx.test.ext.junit.ktx
|
||||
androidTestImplementation testLibs.mockito.android
|
||||
androidTestImplementation testLibs.mockito.kotlin
|
||||
androidTestImplementation testLibs.mockk.android
|
||||
androidTestImplementation testLibs.square.okhttp.mockserver
|
||||
|
||||
instrumentationImplementation (libs.androidx.fragment.testing) {
|
||||
exclude group: 'androidx.test', module: 'core'
|
||||
}
|
||||
|
||||
testImplementation testLibs.espresso.core
|
||||
|
||||
implementation libs.kotlin.stdlib.jdk8
|
||||
implementation libs.kotlin.reflect
|
||||
implementation libs.jackson.module.kotlin
|
||||
|
||||
implementation libs.rxjava3.rxandroid
|
||||
implementation libs.rxjava3.rxkotlin
|
||||
implementation libs.rxdogtag
|
||||
|
||||
androidTestUtil testLibs.androidx.test.orchestrator
|
||||
|
||||
implementation project(':core-ui')
|
||||
ktlintRuleset libs.ktlint.twitter.compose
|
||||
}
|
||||
|
||||
def getLastCommitTimestamp() {
|
||||
if (!(new File('.git').exists())) {
|
||||
return System.currentTimeMillis().toString()
|
||||
}
|
||||
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
exec {
|
||||
executable = 'git'
|
||||
args = ['log', '-1', '--pretty=format:%ct']
|
||||
standardOutput = os
|
||||
}
|
||||
|
||||
return os.toString() + "000"
|
||||
}
|
||||
}
|
||||
|
||||
def getGitHash() {
|
||||
if (!(new File('.git').exists())) {
|
||||
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
||||
}
|
||||
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', 'HEAD'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim().substring(0, 12)
|
||||
}
|
||||
|
||||
def getCurrentGitTag() {
|
||||
if (!(new File('.git').exists())) {
|
||||
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
||||
}
|
||||
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'tag', '--points-at', 'HEAD'
|
||||
standardOutput = stdout
|
||||
}
|
||||
|
||||
def output = stdout.toString().trim()
|
||||
|
||||
if (output != null && output.size() > 0) {
|
||||
def tags = output.split('\n').toList()
|
||||
return tags.stream().filter(t -> t.contains('nightly')).findFirst().orElse(tags.get(0))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
testLogging {
|
||||
events "failed"
|
||||
exceptionFormat "full"
|
||||
showCauses true
|
||||
showExceptions true
|
||||
showStackTraces true
|
||||
}
|
||||
}
|
||||
|
||||
def loadKeystoreProperties(filename) {
|
||||
def keystorePropertiesFile = file("${project.rootDir}/${filename}")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
return keystoreProperties
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
static def getDateSuffix() {
|
||||
def date = new Date()
|
||||
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
|
||||
return formattedDate
|
||||
}
|
||||
|
||||
def getMapsKey() {
|
||||
def mapKey = file("${project.rootDir}/maps.key")
|
||||
if (mapKey.exists()) {
|
||||
return mapKey.readLines()[0]
|
||||
}
|
||||
return "AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"
|
||||
}
|
||||
747
app/build.gradle.kts
Normal file
747
app/build.gradle.kts
Normal file
@@ -0,0 +1,747 @@
|
||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileInputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("androidx.navigation.safeargs")
|
||||
id("org.jlleitschuh.gradle.ktlint")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("app.cash.exhaustive")
|
||||
id("kotlin-parcelize")
|
||||
id("com.squareup.wire")
|
||||
id("translations")
|
||||
id("licenses")
|
||||
}
|
||||
|
||||
apply(from = "static-ips.gradle.kts")
|
||||
|
||||
val canonicalVersionCode = 1454
|
||||
val canonicalVersionName = "7.16.2"
|
||||
val currentHotfixVersion = 0
|
||||
val maxHotfixVersions = 100
|
||||
|
||||
val keystores: Map<String, Properties?> = mapOf("debug" to loadKeystoreProperties("keystore.debug.properties"))
|
||||
|
||||
val selectableVariants = listOf(
|
||||
"nightlyProdSpinner",
|
||||
"nightlyProdPerf",
|
||||
"nightlyProdRelease",
|
||||
"nightlyStagingRelease",
|
||||
"playProdDebug",
|
||||
"playProdSpinner",
|
||||
"playProdCanary",
|
||||
"playProdPerf",
|
||||
"playProdBenchmark",
|
||||
"playProdInstrumentation",
|
||||
"playProdRelease",
|
||||
"playStagingDebug",
|
||||
"playStagingCanary",
|
||||
"playStagingSpinner",
|
||||
"playStagingPerf",
|
||||
"playStagingInstrumentation",
|
||||
"playStagingRelease",
|
||||
"websiteProdSpinner",
|
||||
"websiteProdRelease"
|
||||
)
|
||||
|
||||
val signalBuildToolsVersion: String by rootProject.extra
|
||||
val signalCompileSdkVersion: String by rootProject.extra
|
||||
val signalTargetSdkVersion: Int by rootProject.extra
|
||||
val signalMinSdkVersion: Int by rootProject.extra
|
||||
val signalNdkVersion: String by rootProject.extra
|
||||
val signalJavaVersion: JavaVersion by rootProject.extra
|
||||
val signalKotlinJvmTarget: String by rootProject.extra
|
||||
|
||||
wire {
|
||||
kotlin {
|
||||
javaInterop = true
|
||||
}
|
||||
|
||||
sourcePath {
|
||||
srcDir("src/main/protowire")
|
||||
}
|
||||
|
||||
protoPath {
|
||||
srcDir("${project.rootDir}/libsignal-service/src/main/protowire")
|
||||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
version.set("1.2.1")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.thoughtcrime.securesms"
|
||||
|
||||
buildToolsVersion = signalBuildToolsVersion
|
||||
compileSdkVersion = signalCompileSdkVersion
|
||||
ndkVersion = signalNdkVersion
|
||||
|
||||
flavorDimensions += listOf("distribution", "environment")
|
||||
useLibrary("org.apache.http.legacy")
|
||||
testBuildType = "instrumentation"
|
||||
|
||||
android.bundle.language.enableSplit = false
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = signalKotlinJvmTarget
|
||||
}
|
||||
|
||||
keystores["debug"]?.let { properties ->
|
||||
signingConfigs.getByName("debug").apply {
|
||||
storeFile = file("${project.rootDir}/${properties.getProperty("storeFile")}")
|
||||
storePassword = properties.getProperty("storePassword")
|
||||
keyAlias = properties.getProperty("keyAlias")
|
||||
keyPassword = properties.getProperty("keyPassword")
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
}
|
||||
|
||||
managedDevices {
|
||||
devices {
|
||||
create<ManagedVirtualDevice>("pixel3api30") {
|
||||
device = "Pixel 3"
|
||||
apiLevel = 30
|
||||
systemImageSource = "google-atd"
|
||||
require64Bit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("test") {
|
||||
java.srcDir("$projectDir/src/testShared")
|
||||
}
|
||||
|
||||
getByName("androidTest") {
|
||||
java.srcDir("$projectDir/src/testShared")
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = signalJavaVersion
|
||||
targetCompatibility = signalJavaVersion
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
excludes += setOf(
|
||||
"**/*.dylib",
|
||||
"**/*.dll"
|
||||
)
|
||||
}
|
||||
resources {
|
||||
excludes += setOf(
|
||||
"LICENSE.txt",
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
"asm-license.txt",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/LICENSE.md",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/LICENSE-notice.md",
|
||||
"META-INF/proguard/androidx-annotations.pro",
|
||||
"**/*.dylib",
|
||||
"**/*.dll"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.4"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode = (canonicalVersionCode * maxHotfixVersions) + currentHotfixVersion
|
||||
versionName = canonicalVersionName
|
||||
|
||||
minSdk = signalMinSdkVersion
|
||||
targetSdk = signalTargetSdkVersion
|
||||
|
||||
multiDexEnabled = true
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
project.ext.set("archivesBaseName", "Signal")
|
||||
|
||||
manifestPlaceholders["mapsKey"] = "AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"
|
||||
|
||||
buildConfigField("long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L")
|
||||
buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
|
||||
buildConfigField("String", "SIGNAL_URL", "\"https://chat.signal.org\"")
|
||||
buildConfigField("String", "STORAGE_URL", "\"https://storage.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\"")
|
||||
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_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\"")
|
||||
buildConfigField("String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}")
|
||||
buildConfigField("String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\", \"https://sfu.staging.test.voip.signal.org\"}")
|
||||
buildConfigField("String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"")
|
||||
buildConfigField("int", "CONTENT_PROXY_PORT", "443")
|
||||
buildConfigField("String[]", "SIGNAL_SERVICE_IPS", rootProject.extra["service_ips"] as String)
|
||||
buildConfigField("String[]", "SIGNAL_STORAGE_IPS", rootProject.extra["storage_ips"] as String)
|
||||
buildConfigField("String[]", "SIGNAL_CDN_IPS", rootProject.extra["cdn_ips"] as String)
|
||||
buildConfigField("String[]", "SIGNAL_CDN2_IPS", rootProject.extra["cdn2_ips"] as String)
|
||||
buildConfigField("String[]", "SIGNAL_CDN3_IPS", rootProject.extra["cdn3_ips"] as String)
|
||||
buildConfigField("String[]", "SIGNAL_SFU_IPS", rootProject.extra["sfu_ips"] as String)
|
||||
buildConfigField("String[]", "SIGNAL_CONTENT_PROXY_IPS", rootProject.extra["content_proxy_ips"] as String)
|
||||
buildConfigField("String[]", "SIGNAL_CDSI_IPS", rootProject.extra["cdsi_ips"] as String)
|
||||
buildConfigField("String[]", "SIGNAL_SVR2_IPS", rootProject.extra["svr2_ips"] as String)
|
||||
buildConfigField("String", "SIGNAL_AGENT", "\"OWA\"")
|
||||
buildConfigField("String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"")
|
||||
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O\"")
|
||||
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().map { "\"$it\"" }.joinToString(separator = ", ")} }")
|
||||
buildConfigField("int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode")
|
||||
buildConfigField("String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\"")
|
||||
buildConfigField("String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\"")
|
||||
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\"")
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"unset\"")
|
||||
buildConfigField("String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\"")
|
||||
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
|
||||
buildConfigField("boolean", "TRACING_ENABLED", "false")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
|
||||
|
||||
ndk {
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
}
|
||||
resourceConfigurations += listOf()
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = !project.hasProperty("generateBaselineProfile")
|
||||
reset()
|
||||
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
isUniversalApk = true
|
||||
}
|
||||
}
|
||||
|
||||
testInstrumentationRunner = "org.thoughtcrime.securesms.testing.SignalTestRunner"
|
||||
testInstrumentationRunnerArguments["clearPackageData"] = "true"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
if (keystores["debug"] != null) {
|
||||
signingConfig = signingConfigs["debug"]
|
||||
}
|
||||
isDefault = true
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
"proguard/proguard-firebase-messaging.pro",
|
||||
"proguard/proguard-google-play-services.pro",
|
||||
"proguard/proguard-jackson.pro",
|
||||
"proguard/proguard-sqlite.pro",
|
||||
"proguard/proguard-appcompat-v7.pro",
|
||||
"proguard/proguard-square-okhttp.pro",
|
||||
"proguard/proguard-square-okio.pro",
|
||||
"proguard/proguard-rounded-image-view.pro",
|
||||
"proguard/proguard-glide.pro",
|
||||
"proguard/proguard-shortcutbadger.pro",
|
||||
"proguard/proguard-retrofit.pro",
|
||||
"proguard/proguard-webrtc.pro",
|
||||
"proguard/proguard-klinker.pro",
|
||||
"proguard/proguard-mobilecoin.pro",
|
||||
"proguard/proguard-retrolambda.pro",
|
||||
"proguard/proguard-okhttp.pro",
|
||||
"proguard/proguard-ez-vcard.pro",
|
||||
"proguard/proguard.cfg"
|
||||
)
|
||||
testProguardFiles(
|
||||
"proguard/proguard-automation.pro",
|
||||
"proguard/proguard.cfg"
|
||||
)
|
||||
|
||||
manifestPlaceholders["mapsKey"] = getMapsKey()
|
||||
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Debug\"")
|
||||
}
|
||||
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(*buildTypes["debug"].proguardFiles.toTypedArray())
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Release\"")
|
||||
}
|
||||
|
||||
create("instrumentation") {
|
||||
initWith(getByName("debug"))
|
||||
isDefault = false
|
||||
isMinifyEnabled = false
|
||||
matchingFallbacks += "debug"
|
||||
applicationIdSuffix = ".instrumentation"
|
||||
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Instrumentation\"")
|
||||
}
|
||||
|
||||
create("spinner") {
|
||||
initWith(getByName("debug"))
|
||||
isDefault = false
|
||||
isMinifyEnabled = false
|
||||
matchingFallbacks += "debug"
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Spinner\"")
|
||||
}
|
||||
|
||||
create("perf") {
|
||||
initWith(getByName("debug"))
|
||||
isDefault = false
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
matchingFallbacks += "debug"
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Perf\"")
|
||||
buildConfigField("boolean", "TRACING_ENABLED", "true")
|
||||
}
|
||||
|
||||
create("benchmark") {
|
||||
initWith(getByName("debug"))
|
||||
isDefault = false
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = true
|
||||
matchingFallbacks += "debug"
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Benchmark\"")
|
||||
buildConfigField("boolean", "TRACING_ENABLED", "true")
|
||||
}
|
||||
|
||||
create("canary") {
|
||||
initWith(getByName("debug"))
|
||||
isDefault = false
|
||||
isMinifyEnabled = false
|
||||
matchingFallbacks += "debug"
|
||||
buildConfigField("String", "BUILD_VARIANT_TYPE", "\"Canary\"")
|
||||
}
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
create("play") {
|
||||
dimension = "distribution"
|
||||
isDefault = true
|
||||
buildConfigField("boolean", "MANAGES_APP_UPDATES", "false")
|
||||
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "null")
|
||||
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"play\"")
|
||||
}
|
||||
|
||||
create("website") {
|
||||
dimension = "distribution"
|
||||
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
|
||||
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "\"https://updates.signal.org/android/latest.json\"")
|
||||
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"website\"")
|
||||
}
|
||||
|
||||
create("nightly") {
|
||||
val apkUpdateManifestUrl = if (file("${project.rootDir}/nightly-url.txt").exists()) {
|
||||
file("${project.rootDir}/nightly-url.txt").readText().trim()
|
||||
} else {
|
||||
"<unset>"
|
||||
}
|
||||
|
||||
dimension = "distribution"
|
||||
versionNameSuffix = "-nightly-untagged-${getDateSuffix()}"
|
||||
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
|
||||
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "\"${apkUpdateManifestUrl}\"")
|
||||
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\"")
|
||||
}
|
||||
|
||||
create("prod") {
|
||||
dimension = "environment"
|
||||
|
||||
isDefault = true
|
||||
|
||||
buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\"")
|
||||
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Prod\"")
|
||||
}
|
||||
|
||||
create("staging") {
|
||||
dimension = "environment"
|
||||
|
||||
applicationIdSuffix = ".staging"
|
||||
|
||||
buildConfigField("String", "SIGNAL_URL", "\"https://chat.staging.signal.org\"")
|
||||
buildConfigField("String", "STORAGE_URL", "\"https://storage-staging.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\"")
|
||||
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_SVR2_URL", "\"https://svr2.staging.signal.org\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
|
||||
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==\"")
|
||||
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"")
|
||||
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8\"")
|
||||
buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"")
|
||||
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\"")
|
||||
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = true
|
||||
baseline = file("lint-baseline.xml")
|
||||
checkReleaseBuilds = false
|
||||
disable += "LintError"
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
outputs
|
||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
if (output.baseName.contains("nightly")) {
|
||||
var tag = getCurrentGitTag()
|
||||
if (!tag.isNullOrEmpty()) {
|
||||
if (tag.startsWith("v")) {
|
||||
tag = tag.substring(1)
|
||||
}
|
||||
output.versionNameOverride = tag
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-${output.versionNameOverride}.apk")
|
||||
} else {
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
|
||||
}
|
||||
} else {
|
||||
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
|
||||
|
||||
if (currentHotfixVersion >= maxHotfixVersions) {
|
||||
throw AssertionError("Hotfix version is too large!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants { variant ->
|
||||
variant.enable = variant.name in selectableVariants
|
||||
}
|
||||
onVariants { variant ->
|
||||
// Include the test-only library on debug builds.
|
||||
if (variant.buildType != "instrumentation") {
|
||||
variant.packaging.jniLibs.excludes.add("**/libsignal_jni_testing.so")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val releaseDir = "$projectDir/src/release/java"
|
||||
val debugDir = "$projectDir/src/debug/java"
|
||||
|
||||
android.buildTypes.configureEach {
|
||||
val path = if (name == "release") releaseDir else debugDir
|
||||
sourceSets.named(name) {
|
||||
java.srcDir(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
lintChecks(project(":lintchecks"))
|
||||
ktlintRuleset(libs.ktlint.twitter.compose)
|
||||
coreLibraryDesugaring(libs.android.tools.desugar)
|
||||
|
||||
implementation(project(":libsignal-service"))
|
||||
implementation(project(":paging"))
|
||||
implementation(project(":core-util"))
|
||||
implementation(project(":glide-config"))
|
||||
implementation(project(":video"))
|
||||
implementation(project(":device-transfer"))
|
||||
implementation(project(":image-editor"))
|
||||
implementation(project(":donations"))
|
||||
implementation(project(":contacts"))
|
||||
implementation(project(":qr"))
|
||||
implementation(project(":sticky-header-grid"))
|
||||
implementation(project(":photoview"))
|
||||
implementation(project(":core-ui"))
|
||||
|
||||
implementation(libs.androidx.fragment.ktx)
|
||||
implementation(libs.androidx.appcompat) {
|
||||
version {
|
||||
strictly("1.6.1")
|
||||
}
|
||||
}
|
||||
implementation(libs.androidx.window.window)
|
||||
implementation(libs.androidx.window.java)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation(libs.material.material)
|
||||
implementation(libs.androidx.legacy.support)
|
||||
implementation(libs.androidx.preference)
|
||||
implementation(libs.androidx.legacy.preference)
|
||||
implementation(libs.androidx.gridlayout)
|
||||
implementation(libs.androidx.exifinterface)
|
||||
implementation(libs.androidx.compose.rxjava3)
|
||||
implementation(libs.androidx.compose.runtime.livedata)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
implementation(libs.androidx.multidex)
|
||||
implementation(libs.androidx.navigation.fragment.ktx)
|
||||
implementation(libs.androidx.navigation.ui.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
implementation(libs.androidx.lifecycle.livedata.ktx)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
|
||||
implementation(libs.androidx.lifecycle.common.java8)
|
||||
implementation(libs.androidx.lifecycle.reactivestreams.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.camera.core)
|
||||
implementation(libs.androidx.camera.camera2)
|
||||
implementation(libs.androidx.camera.extensions)
|
||||
implementation(libs.androidx.camera.lifecycle)
|
||||
implementation(libs.androidx.camera.view)
|
||||
implementation(libs.androidx.concurrent.futures)
|
||||
implementation(libs.androidx.autofill)
|
||||
implementation(libs.androidx.biometric)
|
||||
implementation(libs.androidx.sharetarget)
|
||||
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")
|
||||
exclude(group = "com.google.firebase", module = "firebase-measurement-connector")
|
||||
}
|
||||
implementation(libs.google.play.services.maps)
|
||||
implementation(libs.google.play.services.auth)
|
||||
implementation(libs.bundles.media3)
|
||||
implementation(libs.conscrypt.android)
|
||||
implementation(libs.signal.aesgcmprovider)
|
||||
implementation(libs.libsignal.android)
|
||||
implementation(libs.mobilecoin)
|
||||
implementation(libs.signal.ringrtc)
|
||||
implementation(libs.leolin.shortcutbadger)
|
||||
implementation(libs.emilsjolander.stickylistheaders)
|
||||
implementation(libs.apache.httpclient.android)
|
||||
implementation(libs.glide.glide)
|
||||
implementation(libs.roundedimageview)
|
||||
implementation(libs.materialish.progress)
|
||||
implementation(libs.greenrobot.eventbus)
|
||||
implementation(libs.google.zxing.android.integration)
|
||||
implementation(libs.google.zxing.core)
|
||||
implementation(libs.google.flexbox)
|
||||
implementation(libs.subsampling.scale.image.view) {
|
||||
exclude(group = "com.android.support", module = "support-annotations")
|
||||
}
|
||||
implementation(libs.android.tooltips) {
|
||||
exclude(group = "com.android.support", module = "appcompat-v7")
|
||||
}
|
||||
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) {
|
||||
exclude(group = "com.fasterxml.jackson.core")
|
||||
exclude(group = "org.freemarker")
|
||||
}
|
||||
implementation(libs.dnsjava)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.kotlin.stdlib.jdk8)
|
||||
implementation(libs.kotlin.reflect)
|
||||
implementation(libs.kotlinx.coroutines.play.services)
|
||||
implementation(libs.kotlinx.coroutines.rx3)
|
||||
implementation(libs.jackson.module.kotlin)
|
||||
implementation(libs.rxjava3.rxandroid)
|
||||
implementation(libs.rxjava3.rxkotlin)
|
||||
implementation(libs.rxdogtag)
|
||||
|
||||
"playImplementation"(project(":billing"))
|
||||
"nightlyImplementation"(project(":billing"))
|
||||
|
||||
"spinnerImplementation"(project(":spinner"))
|
||||
|
||||
"canaryImplementation"(libs.square.leakcanary)
|
||||
|
||||
"instrumentationImplementation"(libs.androidx.fragment.testing) {
|
||||
exclude(group = "androidx.test", module = "core")
|
||||
}
|
||||
|
||||
testImplementation(testLibs.junit.junit)
|
||||
testImplementation(testLibs.assertj.core)
|
||||
testImplementation(testLibs.mockito.core)
|
||||
testImplementation(testLibs.mockito.kotlin)
|
||||
testImplementation(testLibs.androidx.test.core)
|
||||
testImplementation(testLibs.robolectric.robolectric) {
|
||||
exclude(group = "com.google.protobuf", module = "protobuf-java")
|
||||
}
|
||||
testImplementation(testLibs.robolectric.shadows.multidex)
|
||||
testImplementation(testLibs.bouncycastle.bcprov.jdk15on) {
|
||||
version {
|
||||
strictly("1.70")
|
||||
}
|
||||
}
|
||||
testImplementation(testLibs.bouncycastle.bcpkix.jdk15on) {
|
||||
version {
|
||||
strictly("1.70")
|
||||
}
|
||||
}
|
||||
testImplementation(testLibs.conscrypt.openjdk.uber)
|
||||
testImplementation(testLibs.hamcrest.hamcrest)
|
||||
testImplementation(testLibs.mockk)
|
||||
testImplementation(testFixtures(project(":libsignal-service")))
|
||||
testImplementation(testLibs.espresso.core)
|
||||
|
||||
androidTestImplementation(testLibs.androidx.test.ext.junit)
|
||||
androidTestImplementation(testLibs.espresso.core)
|
||||
androidTestImplementation(testLibs.androidx.test.core)
|
||||
androidTestImplementation(testLibs.androidx.test.core.ktx)
|
||||
androidTestImplementation(testLibs.androidx.test.ext.junit.ktx)
|
||||
androidTestImplementation(testLibs.mockito.android)
|
||||
androidTestImplementation(testLibs.mockito.kotlin)
|
||||
androidTestImplementation(testLibs.mockk.android)
|
||||
androidTestImplementation(testLibs.square.okhttp.mockserver)
|
||||
androidTestImplementation(testLibs.diff.utils)
|
||||
|
||||
androidTestUtil(testLibs.androidx.test.orchestrator)
|
||||
}
|
||||
|
||||
fun assertIsGitRepo() {
|
||||
if (!file("${project.rootDir}/.git").exists()) {
|
||||
throw IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
|
||||
}
|
||||
}
|
||||
|
||||
fun getLastCommitTimestamp(): String {
|
||||
assertIsGitRepo()
|
||||
|
||||
ByteArrayOutputStream().use { os ->
|
||||
exec {
|
||||
executable = "git"
|
||||
args = listOf("log", "-1", "--pretty=format:%ct")
|
||||
standardOutput = os
|
||||
}
|
||||
|
||||
return os.toString() + "000"
|
||||
}
|
||||
}
|
||||
|
||||
fun getGitHash(): String {
|
||||
assertIsGitRepo()
|
||||
|
||||
val stdout = ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine = listOf("git", "rev-parse", "HEAD")
|
||||
standardOutput = stdout
|
||||
}
|
||||
|
||||
return stdout.toString().trim().substring(0, 12)
|
||||
}
|
||||
|
||||
fun getCurrentGitTag(): String? {
|
||||
assertIsGitRepo()
|
||||
|
||||
val stdout = ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine = listOf("git", "tag", "--points-at", "HEAD")
|
||||
standardOutput = stdout
|
||||
}
|
||||
|
||||
val output: String = stdout.toString().trim()
|
||||
|
||||
return if (output.isNotEmpty()) {
|
||||
val tags = output.split("\n").toList()
|
||||
tags.firstOrNull { it.contains("nightly") } ?: tags[0]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
testLogging {
|
||||
events("failed")
|
||||
exceptionFormat = TestExceptionFormat.FULL
|
||||
showCauses = true
|
||||
showExceptions = true
|
||||
showStackTraces = true
|
||||
}
|
||||
}
|
||||
|
||||
project.tasks.configureEach {
|
||||
if (name.lowercase().contains("nightly") && name != "checkNightlyParams") {
|
||||
dependsOn(tasks.getByName("checkNightlyParams"))
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("checkNightlyParams") {
|
||||
doFirst {
|
||||
if (project.gradle.startParameter.taskNames.any { it.lowercase().contains("nightly") }) {
|
||||
|
||||
if (!file("${project.rootDir}/nightly-url.txt").exists()) {
|
||||
throw GradleException("Cannot find 'nightly-url.txt' for nightly build! It must exist in the root of this project and contain the location of the nightly manifest.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadKeystoreProperties(filename: String): Properties? {
|
||||
val keystorePropertiesFile = file("${project.rootDir}/$filename")
|
||||
|
||||
return if (keystorePropertiesFile.exists()) {
|
||||
val keystoreProperties = Properties()
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
keystoreProperties
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getDateSuffix(): String {
|
||||
return SimpleDateFormat("yyyy-MM-dd-HH:mm").format(Date())
|
||||
}
|
||||
|
||||
fun getMapsKey(): String {
|
||||
val mapKey = file("${project.rootDir}/maps.key")
|
||||
|
||||
return if (mapKey.exists()) {
|
||||
mapKey.readLines()[0]
|
||||
} else {
|
||||
"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"
|
||||
}
|
||||
}
|
||||
|
||||
fun Project.languageList(): List<String> {
|
||||
return fileTree("src/main/res") { include("**/strings.xml") }
|
||||
.map { stringFile -> stringFile.parentFile.name }
|
||||
.map { valuesFolderName -> valuesFolderName.replace("values-", "") }
|
||||
.filter { valuesFolderName -> valuesFolderName != "values" }
|
||||
.map { languageCode -> languageCode.replace("-r", "_") }
|
||||
.distinct()
|
||||
.sorted() + "en"
|
||||
}
|
||||
|
||||
fun String.capitalize(): String {
|
||||
return this.replaceFirstChar { it.uppercase() }
|
||||
}
|
||||
@@ -2,8 +2,11 @@
|
||||
-dontobfuscate
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class org.whispersystems.** { *; }
|
||||
-keep class org.signal.libsignal.net.** { *; }
|
||||
-keep class org.signal.libsignal.protocol.** { *; }
|
||||
-keep class org.signal.libsignal.usernames.** { *; }
|
||||
-keep class org.thoughtcrime.securesms.** { *; }
|
||||
-keep class org.signal.donations.json.** { *; }
|
||||
-keepclassmembers class ** {
|
||||
public void onEvent*(**);
|
||||
}
|
||||
@@ -11,4 +14,15 @@
|
||||
# Protobuf lite
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
-keep class androidx.window.** { *; }
|
||||
-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
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
-dontwarn sun.net.spi.nameservice.NameService
|
||||
-dontwarn sun.net.spi.nameservice.NameServiceDescriptor
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
app/src/androidTest/assets/images/sample_image.png
Normal file
BIN
app/src/androidTest/assets/images/sample_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
File diff suppressed because one or more lines are too long
@@ -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,20 +21,32 @@ 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() {
|
||||
persistentLogger = PersistentLogger(this)
|
||||
|
||||
Log.initialize({ true }, AndroidLogger(), persistentLogger, inMemoryLogger)
|
||||
Log.initialize({ true }, AndroidLogger(), PersistentLogger(this), inMemoryLogger)
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(CustomSignalProtocolLogger())
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute {
|
||||
Log.blockUntilAllWritesFinished()
|
||||
LogDatabase.getInstance(this).trimToSize()
|
||||
LogDatabase.getInstance(this).logs.trimToSize()
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginJobLoop() = Unit
|
||||
|
||||
/**
|
||||
* Some of the jobs can interfere with some of the instrumentation tests.
|
||||
*
|
||||
* For example, we may try to create a release channel recipient while doing
|
||||
* an import/backup test.
|
||||
*
|
||||
* This can be used to start the job loop if needed for tests that rely on it.
|
||||
*/
|
||||
fun beginJobLoopForTests() {
|
||||
super.beginJobLoop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.github.difflib.DiffUtils
|
||||
import com.github.difflib.UnifiedDiffUtils
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readFully
|
||||
import org.signal.libsignal.messagebackup.ComparableBackup
|
||||
import org.signal.libsignal.messagebackup.MessageBackup
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.proto.Frame
|
||||
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
|
||||
import org.thoughtcrime.securesms.database.DistributionListTables
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ArchiveImportExportTests {
|
||||
|
||||
companion object {
|
||||
const val TAG = "ImportExport"
|
||||
const val TESTS_FOLDER = "backupTests"
|
||||
|
||||
val SELF_ACI = ServiceId.ACI.from(UUID(100, 100))
|
||||
val SELF_PNI = ServiceId.PNI.from(UUID(101, 101))
|
||||
val SELF_E164 = "+10000000000"
|
||||
val SELF_PROFILE_KEY: ByteArray = Base64.decode("YQKRq+3DQklInaOaMcmlzZnN0m/1hzLiaONX7gB12dg=")
|
||||
val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun all() {
|
||||
runTests()
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun accountData() {
|
||||
runTests { it.startsWith("account_data_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun recipientContacts() {
|
||||
runTests { it.startsWith("recipient_contacts_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun recipientDistributionLists() {
|
||||
runTests { it.startsWith("recipient_distribution_list_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun recipientGroups() {
|
||||
runTests { it.startsWith("recipient_groups_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun chatStandardMessageTextOnly() {
|
||||
runTests { it.startsWith("chat_standard_message_text_only_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun chatStandardMessageFormattedText() {
|
||||
runTests { it.startsWith("chat_standard_message_formatted_text_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun chatStandardMessageLongText() {
|
||||
runTests { it.startsWith("chat_standard_message_long_text_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun chatStandardMessageStandardAttachments() {
|
||||
runTests { it.startsWith("chat_standard_message_standard_attachments_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun chatStandardMessageSpecialAttachments() {
|
||||
runTests { it.startsWith("chat_standard_message_special_attachments_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun chatSimpleUpdates() {
|
||||
runTests { it.startsWith("chat_simple_updates_") }
|
||||
}
|
||||
|
||||
@Ignore("Just for debugging")
|
||||
@Test
|
||||
fun chatContactMessage() {
|
||||
runTests { it.startsWith("chat_contact_message_") }
|
||||
}
|
||||
|
||||
private fun runTests(predicate: (String) -> Boolean = { true }) {
|
||||
val testFiles = InstrumentationRegistry.getInstrumentation().context.resources.assets.list(TESTS_FOLDER)!!.filter(predicate)
|
||||
val results: MutableList<TestResult> = mutableListOf()
|
||||
|
||||
Log.d(TAG, "About to run ${testFiles.size} tests.")
|
||||
|
||||
for (filename in testFiles) {
|
||||
Log.d(TAG, "> $filename")
|
||||
val startTime = System.currentTimeMillis()
|
||||
val result = test(filename)
|
||||
results += result
|
||||
|
||||
if (result is TestResult.Success) {
|
||||
Log.d(TAG, " \uD83D\uDFE2 Passed in ${System.currentTimeMillis() - startTime} ms")
|
||||
} else {
|
||||
Log.d(TAG, " \uD83D\uDD34 Failed in ${System.currentTimeMillis() - startTime} ms")
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
.filterIsInstance<TestResult.Failure>()
|
||||
.forEach {
|
||||
Log.e(TAG, "Failure: ${it.name}\n${it.message}")
|
||||
Log.e(TAG, "----------------------------------")
|
||||
Log.e(TAG, "----------------------------------")
|
||||
Log.e(TAG, "----------------------------------")
|
||||
}
|
||||
|
||||
if (results.any { it is TestResult.Failure }) {
|
||||
val successCount = results.count { it is TestResult.Success }
|
||||
val failingTestNames = results.filterIsInstance<TestResult.Failure>().joinToString(separator = "\n") { " \uD83D\uDD34 ${it.name}" }
|
||||
val message = "Some tests failed! Only $successCount/${results.size} passed. Failure details are above. Failing tests:\n$failingTestNames"
|
||||
|
||||
Log.d(TAG, message)
|
||||
throw AssertionError(message)
|
||||
} else {
|
||||
Log.d(TAG, "All ${results.size} tests passed!")
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(filename: String): TestResult {
|
||||
resetAllData()
|
||||
|
||||
val inputFileBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("$TESTS_FOLDER/$filename").readFully(true)
|
||||
|
||||
val importResult = import(inputFileBytes)
|
||||
assertTrue(importResult is ImportResult.Success)
|
||||
val success = importResult as ImportResult.Success
|
||||
|
||||
val generatedBackupData = BackupRepository.debugExport(plaintext = true, currentTime = success.backupTime)
|
||||
checkEquivalent(filename, inputFileBytes, generatedBackupData)?.let { return it }
|
||||
|
||||
return TestResult.Success(filename)
|
||||
}
|
||||
|
||||
private fun resetAllData() {
|
||||
// Need to delete these first to prevent foreign key crash
|
||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.ListTable.TABLE_NAME}")
|
||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.MembershipTable.TABLE_NAME}")
|
||||
|
||||
SqlUtil.getAllTables(SignalDatabase.rawDatabase)
|
||||
.filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB
|
||||
.sorted()
|
||||
.forEach { table ->
|
||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
|
||||
SqlUtil.resetAutoIncrementValue(SignalDatabase.rawDatabase, table)
|
||||
}
|
||||
|
||||
AppDependencies.recipientCache.clear()
|
||||
AppDependencies.recipientCache.clearSelf()
|
||||
RecipientId.clearCache()
|
||||
|
||||
KeyValueDatabase.getInstance(AppDependencies.application).clear()
|
||||
SignalStore.resetCache()
|
||||
|
||||
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.backup.backupTier = MessageBackupTier.PAID
|
||||
}
|
||||
|
||||
private fun import(importData: ByteArray): ImportResult {
|
||||
return BackupRepository.import(
|
||||
length = importData.size.toLong(),
|
||||
inputStreamFactory = { ByteArrayInputStream(importData) },
|
||||
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)),
|
||||
plaintext = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertPassesValidator(testName: String, generatedBackupData: ByteArray): TestResult.Failure? {
|
||||
try {
|
||||
BackupRepository.validate(
|
||||
length = generatedBackupData.size.toLong(),
|
||||
inputStreamFactory = { ByteArrayInputStream(generatedBackupData) },
|
||||
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY))
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return TestResult.Failure(testName, "Generated backup failed validation: ${e.message}")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun checkEquivalent(testName: String, import: ByteArray, export: ByteArray): TestResult.Failure? {
|
||||
val importComparable = try {
|
||||
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, import.inputStream(), import.size.toLong())
|
||||
} catch (e: Exception) {
|
||||
return TestResult.Failure(testName, "Imported backup hit a validation error: ${e.message}")
|
||||
}
|
||||
|
||||
val exportComparable = try {
|
||||
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, export.inputStream(), import.size.toLong())
|
||||
} catch (e: Exception) {
|
||||
return TestResult.Failure(testName, "Exported backup hit a validation error: ${e.message}")
|
||||
}
|
||||
|
||||
if (importComparable.unknownFieldMessages.isNotEmpty()) {
|
||||
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}")
|
||||
}
|
||||
|
||||
if (exportComparable.unknownFieldMessages.isNotEmpty()) {
|
||||
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}")
|
||||
}
|
||||
|
||||
val canonicalImport = importComparable.comparableString
|
||||
val canonicalExport = exportComparable.comparableString
|
||||
|
||||
if (canonicalImport != canonicalExport) {
|
||||
val importLines = canonicalImport.lines()
|
||||
val exportLines = canonicalExport.lines()
|
||||
|
||||
val patch = DiffUtils.diff(importLines, exportLines)
|
||||
val diff = UnifiedDiffUtils.generateUnifiedDiff("Import", "Export", importLines, patch, 3).joinToString(separator = "\n")
|
||||
|
||||
val importFrames = import.toFrames()
|
||||
val exportFrames = export.toFrames()
|
||||
|
||||
val importGroupFramesByMasterKey = importFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey }
|
||||
val exportGroupFramesByMasterKey = exportFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey }
|
||||
|
||||
val groupErrorMessage = StringBuilder()
|
||||
|
||||
for ((importKey, importValue) in importGroupFramesByMasterKey) {
|
||||
if (exportGroupFramesByMasterKey[importKey]?.let { it.snapshot != importValue.snapshot } == true) {
|
||||
groupErrorMessage.append("[$importKey] Snapshot mismatch.\nImport:\n${importValue}\n\nExport:\n${exportGroupFramesByMasterKey[importKey]}\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
return TestResult.Failure(testName, "Imported backup does not match exported backup. Diff:\n$diff\n$groupErrorMessage")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun ByteArray.toFrames(): List<Frame> {
|
||||
return PlainTextBackupReader(this.inputStream(), this.size.toLong()).use { it.asSequence().toList() }
|
||||
}
|
||||
|
||||
private sealed class TestResult(val name: String) {
|
||||
class Success(name: String) : TestResult(name)
|
||||
class Failure(name: String, val message: String) : TestResult(name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.testing.SignalFlakyTest
|
||||
import org.thoughtcrime.securesms.testing.SignalFlakyTestRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class FlakyTestAnnotationTest {
|
||||
|
||||
@get:Rule
|
||||
val flakyTestRule = SignalFlakyTestRule()
|
||||
|
||||
companion object {
|
||||
private var count = 0
|
||||
}
|
||||
|
||||
@SignalFlakyTest
|
||||
@Test
|
||||
fun purposelyFlaky() {
|
||||
count++
|
||||
assertEquals(3, count)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.whispersystems.signalservice.api.util.toByteArray
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
object TestRecipientUtils {
|
||||
|
||||
private var upperGenAci = 13131313L
|
||||
private var lowerGenAci = 0L
|
||||
|
||||
private var upperGenPni = 12121212L
|
||||
private var lowerGenPni = 0L
|
||||
|
||||
private var groupMasterKeyRandom = Random(12345)
|
||||
|
||||
fun generateProfileKey(): ByteArray {
|
||||
return ProfileKeyUtil.createNew().serialize()
|
||||
}
|
||||
|
||||
fun nextPni(): ByteArray {
|
||||
synchronized(this) {
|
||||
lowerGenPni++
|
||||
var uuid = UUID(upperGenPni, lowerGenPni)
|
||||
return uuid.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun nextAci(): ByteArray {
|
||||
synchronized(this) {
|
||||
lowerGenAci++
|
||||
var uuid = UUID(upperGenAci, lowerGenAci)
|
||||
return uuid.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
fun generateGroupMasterKey(): ByteArray {
|
||||
val masterKey = ByteArray(32)
|
||||
groupMasterKeyRandom.nextBytes(masterKey)
|
||||
return masterKey
|
||||
}
|
||||
}
|
||||
@@ -1,402 +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.mockito.kotlin.mock
|
||||
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.pin.KbsRepository
|
||||
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.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
|
||||
private lateinit var kbsRepository: KbsRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
ApplicationDependencies.getSignalServiceAccountManager().setSoTimeoutMillis(1000)
|
||||
kbsRepository = mock()
|
||||
ThreadUtil.runOnMainSync {
|
||||
viewModel = ChangeNumberViewModel(
|
||||
localNumber = harness.self.requireE164(),
|
||||
changeNumberRepository = ChangeNumberRepository(),
|
||||
savedState = SavedStateHandle(),
|
||||
password = SignalStore.account().servicePassword!!,
|
||||
verifyAccountRepository = VerifyAccountRepository(harness.application),
|
||||
kbsRepository = kbsRepository
|
||||
)
|
||||
|
||||
viewModel.setNewCountry(1)
|
||||
viewModel.setNewNationalNumber("5555550102")
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
|
||||
// GIVEN
|
||||
val aci = Recipient.self().requireServiceId()
|
||||
val newPni = ServiceId.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 = ServiceId.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 = ServiceId.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
|
||||
|
||||
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 = ServiceId.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 = ServiceId.from(UUID.randomUUID())
|
||||
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
|
||||
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isSelected
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Delete
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Ignore("Test fails on small screens, requires scrolling.")
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CheckoutFlowActivityTest__RecurringDonations {
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(othersCount = 10)
|
||||
|
||||
private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.RECURRING_DONATION)
|
||||
|
||||
@Test
|
||||
fun givenRecurringDonations_whenILoadScreen_thenIExpectMonthlySelected() {
|
||||
ActivityScenario.launch<CheckoutFlowActivity>(intent)
|
||||
onView(withId(R.id.monthly)).check(matches(isSelected()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoCurrentDonation_whenILoadScreen_thenIExpectContinueButton() {
|
||||
ActivityScenario.launch<CheckoutFlowActivity>(intent)
|
||||
onView(withText("Continue")).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenACurrentDonation_whenILoadScreen_thenIExpectUpgradeButton() {
|
||||
initialiseConfigurationResponse()
|
||||
initialiseActiveSubscription()
|
||||
|
||||
ActivityScenario.launch<CheckoutFlowActivity>(intent)
|
||||
onView(withText(R.string.SubscribeFragment__update_subscription)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.SubscribeFragment__cancel_subscription)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenACurrentDonation_whenIPressCancel_thenIExpectCancellationDialog() {
|
||||
initialiseConfigurationResponse()
|
||||
initialiseActiveSubscription()
|
||||
|
||||
ActivityScenario.launch<CheckoutFlowActivity>(intent)
|
||||
onView(withText(R.string.SubscribeFragment__cancel_subscription)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.SubscribeFragment__cancel_subscription)).perform(ViewActions.click())
|
||||
onView(withText(R.string.SubscribeFragment__confirm_cancellation)).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.SubscribeFragment__confirm)).perform(ViewActions.click())
|
||||
onView(withText(R.string.StripePaymentInProgressFragment__cancelling)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
private fun initialiseConfigurationResponse() {
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/subscription/configuration") {
|
||||
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
|
||||
assets.open("inAppPaymentsTests/configuration.json").use { stream ->
|
||||
MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun initialiseActiveSubscription() {
|
||||
val currency = Currency.getInstance("USD")
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = currency,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
|
||||
)
|
||||
|
||||
InAppPaymentsRepository.setSubscriber(subscriber)
|
||||
SignalStore.inAppPayments.setSubscriberCurrency(currency, subscriber.type)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
|
||||
MockResponse().success(
|
||||
ActiveSubscription(
|
||||
ActiveSubscription.Subscription(
|
||||
200,
|
||||
currency.currencyCode,
|
||||
BigDecimal.ONE,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
true,
|
||||
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||
false,
|
||||
"active",
|
||||
"STRIPE",
|
||||
"CARD",
|
||||
false
|
||||
),
|
||||
null
|
||||
)
|
||||
)
|
||||
},
|
||||
Delete("/v1/subscription/${subscriber.subscriberId.serialize()}") {
|
||||
Thread.sleep(10000)
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,15 @@ 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
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
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
|
||||
@@ -63,7 +65,8 @@ class ConversationItemPreviewer {
|
||||
attachment()
|
||||
}
|
||||
|
||||
val message = IncomingMediaMessage(
|
||||
val message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
@@ -72,7 +75,7 @@ class ConversationItemPreviewer {
|
||||
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
||||
)
|
||||
|
||||
SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
||||
SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
||||
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
@@ -82,7 +85,8 @@ class ConversationItemPreviewer {
|
||||
attachment()
|
||||
}
|
||||
|
||||
val message = IncomingMediaMessage(
|
||||
val message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
@@ -91,7 +95,7 @@ class ConversationItemPreviewer {
|
||||
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
||||
)
|
||||
|
||||
val insert = SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
||||
val insert = SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
|
||||
|
||||
SignalDatabase.attachments.getAttachmentsForMessage(insert.messageId).forEachIndexed { index, attachment ->
|
||||
// if (index != 1) {
|
||||
@@ -133,7 +137,7 @@ class ConversationItemPreviewer {
|
||||
|
||||
private fun attachment(): SignalServiceAttachmentPointer {
|
||||
return SignalServiceAttachmentPointer(
|
||||
ReleaseChannel.CDN_NUMBER,
|
||||
Cdn.CDN_3.cdnNumber,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
"image/webp",
|
||||
null,
|
||||
@@ -142,13 +146,16 @@ class ConversationItemPreviewer {
|
||||
1024,
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
0,
|
||||
Optional.of("/not-there.jpg"),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
System.currentTimeMillis()
|
||||
System.currentTimeMillis(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
|
||||
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
|
||||
@@ -63,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)
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.database.FakeMessageRecords
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class V2ConversationItemShapeTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(othersCount = 10)
|
||||
|
||||
@Test
|
||||
fun givenNextAndPreviousMessageDoNotExist_whenISetMessageShape_thenIExpectSingle() {
|
||||
val testSubject = V2ConversationItemShape(FakeConversationContext())
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
currentMessage = getMessageRecord(),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreviousWithinTimeoutAndNoNext_whenISetMessageShape_thenIExpectEnd() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 2.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
previousMessage = getMessageRecord(prev)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.END
|
||||
val actual = testSubject.setMessageShape(
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNextWithinTimeoutAndNoPrevious_whenISetMessageShape_thenIExpectStart() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 2.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
nextMessage = getMessageRecord(now)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.START
|
||||
val actual = testSubject.setMessageShape(
|
||||
currentMessage = getMessageRecord(prev),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreviousAndNextWithinTimeout_whenISetMessageShape_thenIExpectMiddle() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 2.minutes.inWholeMilliseconds
|
||||
val next = now + 2.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
previousMessage = getMessageRecord(prev),
|
||||
nextMessage = getMessageRecord(next)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.MIDDLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreviousOutsideTimeoutAndNoNext_whenISetMessageShape_thenIExpectSingle() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 4.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
previousMessage = getMessageRecord(prev)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNextOutsideTimeoutAndNoPrevious_whenISetMessageShape_thenIExpectSingle() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 4.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
nextMessage = getMessageRecord(now)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
currentMessage = getMessageRecord(prev),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreviousAndNextOutsideTimeout_whenISetMessageShape_thenIExpectSingle() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 4.minutes.inWholeMilliseconds
|
||||
val next = now + 4.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
previousMessage = getMessageRecord(prev),
|
||||
nextMessage = getMessageRecord(next)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
private fun getMessageRecord(
|
||||
timestamp: Long = System.currentTimeMillis()
|
||||
): MessageRecord {
|
||||
return FakeMessageRecords.buildMediaMmsMessageRecord(
|
||||
dateReceived = timestamp,
|
||||
dateSent = timestamp,
|
||||
dateServer = timestamp
|
||||
)
|
||||
}
|
||||
|
||||
private class FakeConversationContext(
|
||||
private val hasWallpaper: Boolean = false,
|
||||
private val previousMessage: MessageRecord? = null,
|
||||
private val nextMessage: MessageRecord? = null
|
||||
) : V2ConversationContext {
|
||||
|
||||
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
|
||||
override val searchQuery: String? = null
|
||||
override val requestManager: RequestManager = mockk()
|
||||
override val isParentInScroll: Boolean = false
|
||||
override fun getChatColorsData(): ChatColorsDrawable.ChatColorsData = ChatColorsDrawable.ChatColorsData(null, null)
|
||||
|
||||
override fun onStartExpirationTimeout(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun hasWallpaper(): Boolean = hasWallpaper
|
||||
|
||||
override fun getColorizer(): Colorizer = colorizer
|
||||
|
||||
override fun getNextMessage(adapterPosition: Int): MessageRecord? = nextMessage
|
||||
|
||||
override fun getPreviousMessage(adapterPosition: Int): MessageRecord? = previousMessage
|
||||
}
|
||||
|
||||
private object FakeConversationItemClickListener : ConversationAdapter.ItemClickListener {
|
||||
override fun onQuoteClicked(messageRecord: MmsMessageRecord?) = Unit
|
||||
|
||||
override fun onLinkPreviewClicked(linkPreview: LinkPreview) = Unit
|
||||
|
||||
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) = Unit
|
||||
|
||||
override fun onStickerClicked(stickerLocator: StickerLocator) = Unit
|
||||
|
||||
override fun onViewOnceMessageClicked(messageRecord: MmsMessageRecord) = Unit
|
||||
|
||||
override fun onSharedContactDetailsClicked(contact: Contact, avatarTransitionView: View) = Unit
|
||||
|
||||
override fun onAddToContactsClicked(contact: Contact) = Unit
|
||||
|
||||
override fun onMessageSharedContactClicked(choices: MutableList<Recipient>) = Unit
|
||||
|
||||
override fun onInviteSharedContactClicked(choices: MutableList<Recipient>) = Unit
|
||||
|
||||
override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) = Unit
|
||||
|
||||
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) = Unit
|
||||
|
||||
override fun onMessageWithErrorClicked(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun onIncomingIdentityMismatchClicked(recipientId: RecipientId) = Unit
|
||||
|
||||
override fun onRegisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) = Unit
|
||||
|
||||
override fun onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) = Unit
|
||||
|
||||
override fun onVoiceNotePause(uri: Uri) = Unit
|
||||
|
||||
override fun onVoiceNotePlay(uri: Uri, messageId: Long, position: Double) = Unit
|
||||
|
||||
override fun onVoiceNoteSeekTo(uri: Uri, position: Double) = Unit
|
||||
|
||||
override fun onVoiceNotePlaybackSpeedChanged(uri: Uri, speed: Float) = Unit
|
||||
|
||||
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) = Unit
|
||||
|
||||
override fun onChatSessionRefreshLearnMoreClicked() = Unit
|
||||
|
||||
override fun onBadDecryptLearnMoreClicked(author: RecipientId) = Unit
|
||||
|
||||
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) = Unit
|
||||
|
||||
override fun onJoinGroupCallClicked() = Unit
|
||||
|
||||
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) = Unit
|
||||
|
||||
override fun onEnableCallNotificationsClicked() = Unit
|
||||
|
||||
override fun onPlayInlineContent(conversationMessage: ConversationMessage?) = Unit
|
||||
|
||||
override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) = Unit
|
||||
|
||||
override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) = Unit
|
||||
|
||||
override fun onChangeNumberUpdateContact(recipient: Recipient) = Unit
|
||||
|
||||
override fun onChangeProfileNameUpdateContact(recipient: Recipient) = Unit
|
||||
|
||||
override fun onCallToAction(action: String) = Unit
|
||||
|
||||
override fun onDonateClicked() = Unit
|
||||
|
||||
override fun onBlockJoinRequest(recipient: Recipient) = Unit
|
||||
|
||||
override fun onRecipientNameClicked(target: RecipientId) = Unit
|
||||
|
||||
override fun onInviteToSignalClicked() = Unit
|
||||
|
||||
override fun onActivatePaymentsClicked() = Unit
|
||||
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) = Unit
|
||||
|
||||
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) = Unit
|
||||
|
||||
override fun onUrlClicked(url: String): Boolean = false
|
||||
|
||||
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun onGiftBadgeRevealed(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit
|
||||
|
||||
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit
|
||||
|
||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
|
||||
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) = Unit
|
||||
|
||||
override fun onItemClick(item: MultiselectPart?) = Unit
|
||||
|
||||
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) = Unit
|
||||
|
||||
override fun onShowSafetyTips(forGroup: Boolean) = Unit
|
||||
|
||||
override fun onReportSpamLearnMoreClicked() = Unit
|
||||
|
||||
override fun onMessageRequestAcceptOptionsClicked() = Unit
|
||||
|
||||
override fun onItemDoubleClick(item: MultiselectPart) = Unit
|
||||
override fun onPaymentTombstoneClicked() = Unit
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,16 @@ import androidx.test.filters.FlakyTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
import org.thoughtcrime.securesms.mms.MediaStream
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.util.Optional
|
||||
|
||||
@@ -47,23 +51,22 @@ class AttachmentTableTest {
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||
false
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment2,
|
||||
createMediaStream(byteArrayOf(1, 2, 3)),
|
||||
false
|
||||
createMediaStream(byteArrayOf(1, 2, 3))
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
|
||||
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
|
||||
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
|
||||
@FlakyTest
|
||||
@Ignore("test is flaky")
|
||||
@Test
|
||||
fun givenIdenticalAttachmentsInsertedForPreUpload_whenIUpdateAttachmentDataAndSpecifyOnlyModifyThisAttachment_thenIExpectDifferentFileInfos() {
|
||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||
@@ -74,22 +77,92 @@ class AttachmentTableTest {
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
|
||||
true
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
|
||||
)
|
||||
|
||||
SignalDatabase.attachments.updateAttachmentData(
|
||||
attachment2,
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4)),
|
||||
true
|
||||
createMediaStream(byteArrayOf(1, 2, 3, 4))
|
||||
)
|
||||
|
||||
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
|
||||
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
|
||||
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
|
||||
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
|
||||
|
||||
assertNotEquals(attachment1Info, attachment2Info)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given: A previous attachment and two pre-upload attachments with the same data but different transform properties (standard and high).
|
||||
*
|
||||
* When changing content of standard pre-upload attachment to match pre-existing attachment
|
||||
*
|
||||
* Then update standard pre-upload attachment to match previous attachment, do not update high pre-upload attachment, and do
|
||||
* not delete shared pre-upload uri from disk as it is still being used by the high pre-upload attachment.
|
||||
*/
|
||||
@Test
|
||||
fun doNotDeleteDedupedFileIfUsedByAnotherAttachmentWithADifferentTransformProperties() {
|
||||
// GIVEN
|
||||
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
||||
val compressedData = byteArrayOf(1, 2, 3)
|
||||
|
||||
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
|
||||
|
||||
val previousAttachment = createAttachment(1, BlobProvider.getInstance().forData(compressedData).createForSingleSessionInMemory(), AttachmentTable.TransformProperties.empty())
|
||||
val previousDatabaseAttachmentId: AttachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(1, listOf(previousAttachment), emptyList()).values.first()
|
||||
|
||||
val standardQualityPreUpload = createAttachment(1, blobUncompressed, AttachmentTable.TransformProperties.empty())
|
||||
val standardDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(standardQualityPreUpload)
|
||||
|
||||
val highQualityPreUpload = createAttachment(1, blobUncompressed, AttachmentTable.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH))
|
||||
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData))
|
||||
|
||||
// THEN
|
||||
val previousInfo = SignalDatabase.attachments.getDataFileInfo(previousDatabaseAttachmentId)!!
|
||||
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
|
||||
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
|
||||
|
||||
assertNotEquals(standardInfo, highInfo)
|
||||
highInfo.file assertIsNot standardInfo.file
|
||||
highInfo.file.exists() assertIs true
|
||||
}
|
||||
|
||||
/**
|
||||
* Given: Three pre-upload attachments with the same data but different transform properties (1x standard and 2x high).
|
||||
*
|
||||
* When inserting content of high pre-upload attachment.
|
||||
*
|
||||
* Then do not deduplicate with standard pre-upload attachment, but do deduplicate second high insert.
|
||||
*/
|
||||
@Test
|
||||
fun doNotDedupedFileIfUsedByAnotherAttachmentWithADifferentTransformProperties() {
|
||||
// GIVEN
|
||||
val uncompressData = byteArrayOf(1, 2, 3, 4, 5)
|
||||
val blobUncompressed = BlobProvider.getInstance().forData(uncompressData).createForSingleSessionInMemory()
|
||||
|
||||
val standardQualityPreUpload = createAttachment(1, blobUncompressed, AttachmentTable.TransformProperties.empty())
|
||||
val standardDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(standardQualityPreUpload)
|
||||
|
||||
// WHEN
|
||||
val highQualityPreUpload = createAttachment(1, blobUncompressed, AttachmentTable.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH))
|
||||
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
|
||||
|
||||
val secondHighQualityPreUpload = createAttachment(1, blobUncompressed, AttachmentTable.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH))
|
||||
val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload)
|
||||
|
||||
// THEN
|
||||
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
|
||||
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
|
||||
val secondHighInfo = SignalDatabase.attachments.getDataFileInfo(secondHighDatabaseAttachment.attachmentId)!!
|
||||
|
||||
highInfo.file assertIsNot standardInfo.file
|
||||
secondHighInfo.file assertIs highInfo.file
|
||||
standardInfo.file.exists() assertIs true
|
||||
highInfo.file.exists() assertIs true
|
||||
}
|
||||
|
||||
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties): UriAttachment {
|
||||
return UriAttachmentBuilder.build(
|
||||
id,
|
||||
|
||||
@@ -0,0 +1,838 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
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
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
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
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Collection of [AttachmentTable] tests focused around deduping logic.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AttachmentTableTest_deduping {
|
||||
|
||||
companion object {
|
||||
val DATA_A = byteArrayOf(1, 2, 3)
|
||||
val DATA_A_COMPRESSED = byteArrayOf(4, 5, 6)
|
||||
val DATA_A_HASH = byteArrayOf(1, 1, 1)
|
||||
|
||||
val DATA_B = byteArrayOf(7, 8, 9)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalStore.account.setAci(ServiceId.ACI.from(UUID.randomUUID()))
|
||||
SignalStore.account.setPni(ServiceId.PNI.from(UUID.randomUUID()))
|
||||
SignalStore.account.setE164("+15558675309")
|
||||
|
||||
SignalDatabase.attachments.deleteAllAttachments()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates two different files with different data. Should not dedupe.
|
||||
*/
|
||||
@Test
|
||||
fun differentFiles() {
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_B)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts files with identical data but with transform properties that make them incompatible. Should not dedupe.
|
||||
*/
|
||||
@Test
|
||||
fun identicalFiles_incompatibleTransforms() {
|
||||
// Non-matching qualities
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching video trim flag
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties())
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching video trim start time
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching video trim end time
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 1))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Non-matching mp4 fast start
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(mp4FastStart = true))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(mp4FastStart = false))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts files with identical data and compatible transform properties. Should dedupe.
|
||||
*/
|
||||
@Test
|
||||
fun identicalFiles_compatibleTransforms() {
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks through various scenarios where files are compressed and uploaded.
|
||||
*/
|
||||
@Test
|
||||
fun compressionAndUploads() {
|
||||
// Matches after the first is compressed, skip transform properly set
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Matches after the first is uploaded, skip transform and ending hash properly set
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
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.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, false)
|
||||
assertSkipTransform(id2, false)
|
||||
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
|
||||
upload(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Re-use the upload when uploaded recently
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Do not re-use old uploads
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis() - 100.days.inWholeMilliseconds)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, true)
|
||||
|
||||
assertDoesNotHaveRemoteFields(id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// This isn't so much "desirable behavior" as it is documenting how things work.
|
||||
// If an attachment is compressed but not uploaded yet, it will have a DATA_HASH_START that doesn't match the actual file content.
|
||||
// This means that if we insert a new attachment with data that matches the compressed data, we won't find a match.
|
||||
// This is ok because we don't allow forwarding unsent messages, so the chances of the user somehow sending a file that matches data we compressed are very low.
|
||||
// What *is* more common is that the user may send DATA_A again, and in this case we will still catch the dedupe (which is already tested above).
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you forward an already-send compressed attachment. We should match, skip transform, and skip upload.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
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.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
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.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id2, false)
|
||||
assertDoesNotHaveRemoteFields(id2)
|
||||
}
|
||||
|
||||
// This represents what would happen if you sent an image using standard quality, then forwarded it using high quality.
|
||||
// Since you're forwarding, it doesn't matter if the new thing has a higher quality, we should still match and skip transform.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
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.
|
||||
// Since you're forwarding, it doesn't matter if the new thing has a lower quality, we should still match and skip transform.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1, uploadTimestamp = System.currentTimeMillis())
|
||||
|
||||
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertSkipTransform(id1, true)
|
||||
assertSkipTransform(id1, true)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
|
||||
// Make sure that files marked as unhashable are all updated together
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
upload(id2)
|
||||
clearHashes(id1)
|
||||
clearHashes(id2)
|
||||
|
||||
val file = dataFile(id1)
|
||||
SignalDatabase.attachments.markDataFileAsUnhashable(file)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
|
||||
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id1)!!
|
||||
assertTrue(dataFileInfo.hashEnd!!.startsWith("UNHASHABLE-"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Various deletion scenarios to ensure that duped files don't deleted while there's still references.
|
||||
*/
|
||||
@Test
|
||||
fun deletions() {
|
||||
// Delete original then dupe
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
val dataFile = dataFile(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
|
||||
delete(id1)
|
||||
|
||||
assertDeleted(id1)
|
||||
assertRowAndFileExists(id2)
|
||||
assertTrue(dataFile.exists())
|
||||
|
||||
delete(id2)
|
||||
|
||||
assertDeleted(id2)
|
||||
assertFalse(dataFile.exists())
|
||||
}
|
||||
|
||||
// Delete dupe then original
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
val dataFile = dataFile(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
|
||||
delete(id2)
|
||||
assertDeleted(id2)
|
||||
assertRowAndFileExists(id1)
|
||||
assertTrue(dataFile.exists())
|
||||
|
||||
delete(id1)
|
||||
assertDeleted(id1)
|
||||
assertFalse(dataFile.exists())
|
||||
}
|
||||
|
||||
// Delete original after it was compressed
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
|
||||
delete(id1)
|
||||
|
||||
assertDeleted(id1)
|
||||
assertRowAndFileExists(id2)
|
||||
assertSkipTransform(id2, true)
|
||||
}
|
||||
|
||||
// Quotes are weak references and should not prevent us from deleting the file
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
val dataFile = dataFile(id1)
|
||||
|
||||
delete(id1)
|
||||
assertDeleted(id1)
|
||||
assertRowExists(id2)
|
||||
assertFalse(dataFile.exists())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun quotes() {
|
||||
// Basic quote deduping
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashStartMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Making sure remote fields carry
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertQuote(id1)
|
||||
upload(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
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
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A, transformProperties = TransformProperties.forVideoTrim(1, 2))
|
||||
compress(id1, DATA_A_COMPRESSED)
|
||||
upload(id1)
|
||||
|
||||
val id2 = insertQuote(id1)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertRemoteFieldsMatch(id1, id2)
|
||||
assertArchiveFieldsMatch(id1, id2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Suite of tests around the migration where we hash all of the attachments and potentially dedupe them.
|
||||
*/
|
||||
@Test
|
||||
fun migration() {
|
||||
// Verifying that getUnhashedDataFile only returns if there's actually missing hashes
|
||||
test {
|
||||
val id = insertWithData(DATA_A)
|
||||
upload(id)
|
||||
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||
}
|
||||
|
||||
// Verifying that getUnhashedDataFile finds the missing hash
|
||||
test {
|
||||
val id = insertWithData(DATA_A)
|
||||
upload(id)
|
||||
clearHashes(id)
|
||||
assertNotNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||
}
|
||||
|
||||
// Verifying that getUnhashedDataFile doesn't return if the file isn't done downloading
|
||||
test {
|
||||
val id = insertWithData(DATA_A)
|
||||
upload(id)
|
||||
setTransferState(id, AttachmentTable.TRANSFER_PROGRESS_PENDING)
|
||||
clearHashes(id)
|
||||
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
|
||||
}
|
||||
|
||||
// If two attachments share the same file, when we backfill the hash, make sure both get their hashes set
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
val id2 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
upload(id2)
|
||||
|
||||
clearHashes(id1)
|
||||
clearHashes(id2)
|
||||
|
||||
val file = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file, DATA_A_HASH)
|
||||
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
}
|
||||
|
||||
// Creates a situation where two different attachments have the same data but wrote to different files, and verifies the migration dedupes it
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
clearHashes(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
upload(id2)
|
||||
clearHashes(id2)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
|
||||
val file1 = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
|
||||
val file2 = dataFile(id2)
|
||||
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertFalse(file2.exists())
|
||||
}
|
||||
|
||||
// We've got three files now with the same data, with two of them sharing a file. We want to make sure *both* entries that share the same file get deduped.
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
clearHashes(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
val id3 = insertWithData(DATA_A)
|
||||
upload(id2)
|
||||
upload(id3)
|
||||
clearHashes(id2)
|
||||
clearHashes(id3)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertDataFilesAreTheSame(id2, id3)
|
||||
|
||||
val file1 = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
|
||||
val file2 = dataFile(id2)
|
||||
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||
|
||||
assertDataFilesAreTheSame(id1, id2)
|
||||
assertDataHashEndMatches(id1, id2)
|
||||
assertDataHashEndMatches(id2, id3)
|
||||
assertFalse(file2.exists())
|
||||
}
|
||||
|
||||
// We don't want to mess with files that are still downloading, so this makes sure that even if data matches, we don't dedupe and don't delete the file
|
||||
test {
|
||||
val id1 = insertWithData(DATA_A)
|
||||
upload(id1)
|
||||
clearHashes(id1)
|
||||
|
||||
val id2 = insertWithData(DATA_A)
|
||||
// *not* uploaded
|
||||
clearHashes(id2)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
|
||||
val file1 = dataFile(id1)
|
||||
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
|
||||
assertDataHashEnd(id1, DATA_A_HASH)
|
||||
|
||||
val file2 = dataFile(id2)
|
||||
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
|
||||
|
||||
assertDataFilesAreDifferent(id1, id2)
|
||||
assertTrue(file2.exists())
|
||||
}
|
||||
}
|
||||
|
||||
private class TestContext {
|
||||
fun insertWithData(data: ByteArray, transformProperties: TransformProperties = TransformProperties.empty()): AttachmentId {
|
||||
val uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
|
||||
|
||||
val attachment = UriAttachmentBuilder.build(
|
||||
id = Random.nextLong(),
|
||||
uri = uri,
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
transformProperties = transformProperties
|
||||
)
|
||||
|
||||
return SignalDatabase.attachments.insertAttachmentForPreUpload(attachment).attachmentId
|
||||
}
|
||||
|
||||
fun insertQuote(attachmentId: AttachmentId): AttachmentId {
|
||||
val originalAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.self())
|
||||
val messageId = SignalDatabase.messages.insertMessageOutbox(
|
||||
message = OutgoingMessage(
|
||||
threadRecipient = Recipient.self(),
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
body = "some text",
|
||||
outgoingQuote = QuoteModel(
|
||||
id = 123,
|
||||
author = Recipient.self().id,
|
||||
text = "Some quote text",
|
||||
isOriginalMissing = false,
|
||||
attachments = listOf(originalAttachment),
|
||||
mentions = emptyList(),
|
||||
type = QuoteModel.Type.NORMAL,
|
||||
bodyRanges = null
|
||||
)
|
||||
),
|
||||
threadId = threadId,
|
||||
forceSms = false,
|
||||
insertListener = null
|
||||
)
|
||||
|
||||
val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId)
|
||||
return attachments[0].attachmentId
|
||||
}
|
||||
|
||||
fun compress(attachmentId: AttachmentId, newData: ByteArray, mp4FastStart: Boolean = false) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
SignalDatabase.attachments.updateAttachmentData(databaseAttachment, newData.asMediaStream())
|
||||
SignalDatabase.attachments.markAttachmentAsTransformed(attachmentId, withFastStart = mp4FastStart)
|
||||
}
|
||||
|
||||
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) {
|
||||
SignalDatabase.attachments.deleteAttachment(attachmentId)
|
||||
}
|
||||
|
||||
fun dataFile(attachmentId: AttachmentId): File {
|
||||
return SignalDatabase.attachments.getDataFileInfo(attachmentId)!!.file
|
||||
}
|
||||
|
||||
fun setTransferState(attachmentId: AttachmentId, transferState: Int) {
|
||||
// messageId doesn't actually matter -- that's for notifying listeners
|
||||
SignalDatabase.attachments.setTransferState(messageId = -1, attachmentId = attachmentId, transferState = transferState)
|
||||
}
|
||||
|
||||
fun clearHashes(id: AttachmentId) {
|
||||
SignalDatabase.attachments.writableDatabase
|
||||
.update(AttachmentTable.TABLE_NAME)
|
||||
.values(
|
||||
AttachmentTable.DATA_HASH_START to null,
|
||||
AttachmentTable.DATA_HASH_END to null
|
||||
)
|
||||
.where("${AttachmentTable.ID} = ?", id)
|
||||
.run()
|
||||
}
|
||||
|
||||
fun assertDeleted(attachmentId: AttachmentId) {
|
||||
assertNull("$attachmentId exists, but it shouldn't!", SignalDatabase.attachments.getAttachment(attachmentId))
|
||||
}
|
||||
|
||||
fun assertRowAndFileExists(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
assertNotNull("$attachmentId does not exist!", databaseAttachment)
|
||||
|
||||
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(attachmentId)
|
||||
assertTrue("The file for $attachmentId does not exist!", dataFileInfo!!.file.exists())
|
||||
}
|
||||
|
||||
fun assertRowExists(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
assertNotNull("$attachmentId does not exist!", databaseAttachment)
|
||||
}
|
||||
|
||||
fun assertDataFilesAreTheSame(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assert(lhsInfo.file.exists())
|
||||
assert(rhsInfo.file.exists())
|
||||
|
||||
assertEquals(lhsInfo.file, rhsInfo.file)
|
||||
assertEquals(lhsInfo.length, rhsInfo.length)
|
||||
assertArrayEquals(lhsInfo.random, rhsInfo.random)
|
||||
}
|
||||
|
||||
fun assertDataFilesAreDifferent(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assert(lhsInfo.file.exists())
|
||||
assert(rhsInfo.file.exists())
|
||||
|
||||
assertNotEquals(lhsInfo.file, rhsInfo.file)
|
||||
}
|
||||
|
||||
fun assertDataHashStartMatches(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assertNotNull(lhsInfo.hashStart)
|
||||
assertEquals("DATA_HASH_START's did not match!", lhsInfo.hashStart, rhsInfo.hashStart)
|
||||
}
|
||||
|
||||
fun assertDataHashEndMatches(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
|
||||
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
|
||||
|
||||
assertNotNull(lhsInfo.hashEnd)
|
||||
assertEquals("DATA_HASH_END's did not match!", lhsInfo.hashEnd, rhsInfo.hashEnd)
|
||||
}
|
||||
|
||||
fun assertDataHashEnd(id: AttachmentId, byteArray: ByteArray) {
|
||||
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id)!!
|
||||
assertArrayEquals(byteArray, Base64.decode(dataFileInfo.hashEnd!!))
|
||||
}
|
||||
|
||||
fun assertRemoteFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
|
||||
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
|
||||
|
||||
assertEquals(lhsAttachment.remoteLocation, rhsAttachment.remoteLocation)
|
||||
assertEquals(lhsAttachment.remoteKey, rhsAttachment.remoteKey)
|
||||
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
|
||||
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
|
||||
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
|
||||
assertEquals(lhsAttachment.cdn.cdnNumber, rhsAttachment.cdn.cdnNumber)
|
||||
}
|
||||
|
||||
fun assertArchiveFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
|
||||
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
|
||||
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
|
||||
|
||||
assertEquals(lhsAttachment.archiveCdn, rhsAttachment.archiveCdn)
|
||||
assertEquals(lhsAttachment.archiveMediaName, rhsAttachment.archiveMediaName)
|
||||
assertEquals(lhsAttachment.archiveMediaId, rhsAttachment.archiveMediaId)
|
||||
}
|
||||
|
||||
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
assertEquals(0, databaseAttachment.uploadTimestamp)
|
||||
assertNull(databaseAttachment.remoteLocation)
|
||||
assertNull(databaseAttachment.remoteDigest)
|
||||
assertNull(databaseAttachment.remoteKey)
|
||||
assertEquals(0, databaseAttachment.cdn.cdnNumber)
|
||||
}
|
||||
|
||||
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
|
||||
val transformProperties = SignalDatabase.attachments.getTransformProperties(attachmentId)!!
|
||||
assertEquals("Incorrect skipTransform!", transformProperties.skipTransform, state)
|
||||
}
|
||||
|
||||
private fun ByteArray.asMediaStream(): MediaStream {
|
||||
return MediaStream(this.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
|
||||
}
|
||||
|
||||
private fun createPointerAttachment(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): PointerAttachment {
|
||||
val location = "somewhere-${Random.nextLong()}"
|
||||
val key = "somekey-${Random.nextLong()}"
|
||||
val digest = Random.nextBytes(32)
|
||||
val incrementalDigest = Random.nextBytes(16)
|
||||
|
||||
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
|
||||
|
||||
return PointerAttachment(
|
||||
"image/jpeg",
|
||||
AttachmentTable.TRANSFER_PROGRESS_DONE,
|
||||
databaseAttachment.size, // size
|
||||
null,
|
||||
Cdn.CDN_3, // cdnNumber
|
||||
location,
|
||||
key,
|
||||
digest,
|
||||
incrementalDigest,
|
||||
5, // incrementalMacChunkSize
|
||||
null,
|
||||
databaseAttachment.voiceNote,
|
||||
databaseAttachment.borderless,
|
||||
databaseAttachment.videoGif,
|
||||
databaseAttachment.width,
|
||||
databaseAttachment.height,
|
||||
uploadTimestamp,
|
||||
databaseAttachment.caption,
|
||||
databaseAttachment.stickerLocator,
|
||||
databaseAttachment.blurHash,
|
||||
databaseAttachment.uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(content: TestContext.() -> Unit) {
|
||||
SignalDatabase.attachments.deleteAllAttachments()
|
||||
val context = TestContext()
|
||||
context.content()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CallLinkTableTest {
|
||||
|
||||
companion object {
|
||||
private val ROOM_ID_A = byteArrayOf(1, 2, 3, 4)
|
||||
private val ROOM_ID_B = byteArrayOf(2, 2, 3, 4)
|
||||
private const val TIMESTAMP_A = 1000L
|
||||
private const val TIMESTAMP_B = 2000L
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteBeforeFirst_thenIExpectNeitherDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_A - 500)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(2, callEvents.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteOnFirst_thenIExpectFirstDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_A)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(1, callEvents.size)
|
||||
assertEquals(TIMESTAMP_B, callEvents.first().record.timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteAfterFirstAndBeforeSecond_thenIExpectFirstDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_B - 500)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(1, callEvents.size)
|
||||
assertEquals(TIMESTAMP_B, callEvents.first().record.timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteOnSecond_thenIExpectBothDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_B)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(0, callEvents.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteAfterSecond_thenIExpectBothDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_B + 500)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(0, callEvents.size)
|
||||
}
|
||||
|
||||
private fun insertTwoNonAdminCallLinksWithEvents() {
|
||||
insertCallLinkWithEvent(ROOM_ID_A, 1000)
|
||||
insertCallLinkWithEvent(ROOM_ID_B, 2000)
|
||||
}
|
||||
|
||||
private fun insertCallLinkWithEvent(roomId: ByteArray, timestamp: Long) {
|
||||
SignalDatabase.callLinks.insertCallLink(
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = CallLinkRoomId.fromBytes(roomId),
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = roomId,
|
||||
adminPassBytes = null
|
||||
),
|
||||
state = SignalCallLinkState()
|
||||
)
|
||||
)
|
||||
|
||||
val callLinkRecipient = SignalDatabase.recipients.getByCallLinkRoomId(CallLinkRoomId.fromBytes(roomId)).get()
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
1,
|
||||
callLinkRecipient,
|
||||
CallTable.Direction.INCOMING,
|
||||
timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,28 +10,32 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.ringrtc.CallId
|
||||
import org.signal.ringrtc.CallManager
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CallTableTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
private val groupRecipientId: RecipientId
|
||||
get() = harness.group!!.recipientId
|
||||
|
||||
@Test
|
||||
fun givenACall_whenISetTimestamp_thenIExpectUpdatedTimestamp() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.setTimestamp(callId, conversationId, -1L)
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
SignalDatabase.calls.setTimestamp(callId, groupRecipientId, -1L)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(-1L, call?.timestamp)
|
||||
|
||||
@@ -43,19 +47,18 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenPreExistingEvent_whenIDeleteGroupCall_thenIMarkDeletedAndSetTimestamp() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
SignalDatabase.calls.deleteGroupCall(call!!)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
SignalDatabase.calls.markCallDeletedFromSyncEvent(call!!)
|
||||
|
||||
val deletedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val deletedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
|
||||
|
||||
assertEquals(CallTable.Event.DELETE, deletedCall?.event)
|
||||
@@ -66,15 +69,15 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
||||
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
CallTable.Type.GROUP_CALL,
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
|
||||
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
|
||||
@@ -87,15 +90,14 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenIInsertAcceptedOutgoingGroupCall_thenIExpectLocalRingerAndOutgoingRing() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
|
||||
assertEquals(harness.self.id, call?.ringerRecipient)
|
||||
@@ -105,15 +107,14 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenIInsertAcceptedIncomingGroupCall_thenIExpectJoined() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.JOINED, call?.event)
|
||||
assertNull(call?.ringerRecipient)
|
||||
@@ -123,16 +124,15 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenARingingCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
|
||||
@@ -140,23 +140,22 @@ class CallTableTest {
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAMissedCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
|
||||
@@ -164,23 +163,22 @@ class CallTableTest {
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADeclinedCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
|
||||
@@ -188,7 +186,7 @@ class CallTableTest {
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@@ -196,9 +194,8 @@ class CallTableTest {
|
||||
fun givenAGenericGroupCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = System.currentTimeMillis(),
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -206,7 +203,7 @@ class CallTableTest {
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
|
||||
@@ -214,17 +211,134 @@ class CallTableTest {
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.JOINED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() {
|
||||
fun givenAnOutgoingRingCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId = callId,
|
||||
recipientId = groupRecipientId,
|
||||
direction = CallTable.Direction.OUTGOING,
|
||||
timestamp = 1
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = groupRecipientId,
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAMissedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = groupRecipientId,
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADeclinedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = groupRecipientId,
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnAcceptedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = groupRecipientId,
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptIncomingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||
SignalDatabase.calls.getCallById(callId, groupRecipientId)!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGenericGroupCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = System.currentTimeMillis(),
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -232,7 +346,58 @@ class CallTableTest {
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptOutgoingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAJoinedGroupCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = System.currentTimeMillis(),
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
|
||||
SignalDatabase.calls.acceptIncomingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
SignalDatabase.calls.acceptOutgoingGroupCall(SignalDatabase.calls.getCallById(callId, groupRecipientId)!!)
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = System.currentTimeMillis(),
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
}
|
||||
@@ -242,9 +407,8 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -252,13 +416,13 @@ class CallTableTest {
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.getCallById(callId, conversationId).let {
|
||||
SignalDatabase.calls.getCallById(callId, groupRecipientId).let {
|
||||
assertNotNull(it)
|
||||
assertEquals(now, it?.timestamp)
|
||||
}
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = 1L,
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -266,7 +430,7 @@ class CallTableTest {
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
assertEquals(1L, call?.timestamp)
|
||||
@@ -275,23 +439,23 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
||||
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
|
||||
callId = callId,
|
||||
recipientId = harness.others[0],
|
||||
recipientId = groupRecipientId,
|
||||
direction = CallTable.Direction.INCOMING,
|
||||
timestamp = System.currentTimeMillis()
|
||||
timestamp = System.currentTimeMillis(),
|
||||
type = CallTable.Type.GROUP_CALL
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
ringId = callId,
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
ringerRecipient = harness.others[1],
|
||||
dateReceived = System.currentTimeMillis(),
|
||||
ringState = CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DELETE, call?.event)
|
||||
}
|
||||
@@ -301,9 +465,8 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -313,13 +476,13 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
@@ -330,23 +493,22 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
@@ -357,9 +519,8 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -369,13 +530,13 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
@@ -386,9 +547,8 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -398,7 +558,7 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
@@ -406,13 +566,13 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
@@ -423,23 +583,22 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_LOCALLY
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
}
|
||||
@@ -449,23 +608,22 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
}
|
||||
@@ -475,9 +633,8 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -487,7 +644,7 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
@@ -495,13 +652,13 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_LOCALLY
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
}
|
||||
@@ -511,9 +668,8 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -523,7 +679,7 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
@@ -531,13 +687,13 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
}
|
||||
@@ -547,9 +703,8 @@ class CallTableTest {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = harness.others[0],
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
@@ -559,13 +714,13 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
}
|
||||
@@ -574,11 +729,10 @@ class CallTableTest {
|
||||
fun givenARingingCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
@@ -586,13 +740,13 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
}
|
||||
@@ -601,11 +755,10 @@ class CallTableTest {
|
||||
fun givenAMissedCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
@@ -613,13 +766,13 @@ class CallTableTest {
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
}
|
||||
@@ -628,24 +781,23 @@ class CallTableTest {
|
||||
fun givenAnOutgoingRingCallEvent_whenRingDeclinedOnAnotherDevice_thenIDoNotChangeState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
|
||||
}
|
||||
@@ -653,16 +805,15 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingRequested_thenICreateAnEventInTheRingingStateAndSetRinger() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
@@ -672,16 +823,15 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingExpired_thenICreateAnEventInTheMissedStateAndSetRinger() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
@@ -691,16 +841,15 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingCancelledByRinger_thenICreateAnEventInTheMissedStateAndSetRinger() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.CANCELLED_BY_RINGER
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
@@ -710,16 +859,15 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyLocally_thenICreateAnEventInTheMissedState() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_LOCALLY
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
@@ -728,16 +876,15 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyOnAnotherDevice_thenICreateAnEventInTheMissedState() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
@@ -746,16 +893,15 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingAcceptedOnAnotherDevice_thenICreateAnEventInTheAcceptedState() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
@@ -764,18 +910,87 @@ class CallTableTest {
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingDeclinedOnAnotherDevice_thenICreateAnEventInTheDeclinedState() {
|
||||
val callId = 1L
|
||||
val conversationId = CallTable.CallConversationId.Peer(harness.others[0])
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
harness.others[0],
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, conversationId)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteBeforeCallB_thenOnlyDeleteCallA() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(1500)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(1, allCallEvents.size)
|
||||
assertEquals(2, allCallEvents.first().record.callId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteBeforeCallA_thenIDoNotDeleteAnyCalls() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(500)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(2, allCallEvents.size)
|
||||
assertEquals(2, allCallEvents[0].record.callId)
|
||||
assertEquals(1, allCallEvents[1].record.callId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteOnCallA_thenIOnlyDeleteCallA() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(1000)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(1, allCallEvents.size)
|
||||
assertEquals(2, allCallEvents.first().record.callId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteOnCallB_thenIDeleteBothCalls() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(2000)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(0, allCallEvents.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteAfterCallB_thenIDeleteBothCalls() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(2500)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(0, allCallEvents.size)
|
||||
}
|
||||
|
||||
private fun insertTwoCallEvents() {
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
1,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
1000
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
2,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.OUTGOING,
|
||||
2000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,14 @@ import net.zetetic.database.sqlcipher.SQLiteOpenHelper
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ForeignKeyConstraint
|
||||
import org.signal.core.util.Index
|
||||
import org.signal.core.util.getForeignKeys
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -24,9 +28,9 @@ class DatabaseConsistencyTest {
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun 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)
|
||||
}
|
||||
|
||||
@@ -61,6 +65,30 @@ class DatabaseConsistencyTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testForeignKeyIndexCoverage() {
|
||||
/** We may deem certain indexes non-critical if deletion frequency is low or table size is small. */
|
||||
val ignoredColumns: List<Pair<String, String>> = listOf(
|
||||
StorySendTable.TABLE_NAME to StorySendTable.DISTRIBUTION_ID
|
||||
)
|
||||
|
||||
val foreignKeys: List<ForeignKeyConstraint> = SignalDatabase.rawDatabase.getForeignKeys()
|
||||
val indexesByFirstColumn: List<Index> = SignalDatabase.rawDatabase.getIndexes()
|
||||
|
||||
val notFound: List<Pair<String, String>> = foreignKeys
|
||||
.filterNot { ignoredColumns.contains(it.table to it.column) }
|
||||
.filterNot { foreignKey ->
|
||||
indexesByFirstColumn.hasPrimaryIndexFor(foreignKey.table, foreignKey.column)
|
||||
}
|
||||
.map { it.table to it.column }
|
||||
|
||||
assertTrue("Missing indexes to cover: $notFound", notFound.isEmpty())
|
||||
}
|
||||
|
||||
private fun List<Index>.hasPrimaryIndexFor(table: String, column: String): Boolean {
|
||||
return this.any { index -> index.table == table && index.columns[0] == column }
|
||||
}
|
||||
|
||||
private data class Statement(
|
||||
val name: String,
|
||||
val sql: String
|
||||
@@ -74,6 +102,7 @@ class DatabaseConsistencyTest {
|
||||
sql = cursor.requireNonNullString("sql").normalizeSql()
|
||||
)
|
||||
}
|
||||
.filterNot { it.name.startsWith("sqlite_stat") }
|
||||
.sortedBy { it.name }
|
||||
}
|
||||
|
||||
@@ -82,10 +111,11 @@ class DatabaseConsistencyTest {
|
||||
.split("\n")
|
||||
.map { it.trim() }
|
||||
.joinToString(separator = " ")
|
||||
.replace(Regex.fromLiteral(" ,"), ",")
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.replace(Regex.fromLiteral("( "), "(")
|
||||
.replace(Regex.fromLiteral(" )"), ")")
|
||||
.replace(Regex("CREATE TABLE \"([a-z]+)\""), "CREATE TABLE $1") // for some reason SQLite will wrap table names in quotes for upgraded tables. This unwraps them.
|
||||
.replace(Regex("CREATE TABLE \"([a-zA-Z_]+)\""), "CREATE TABLE $1") // for some reason SQLite will wrap table names in quotes for upgraded tables. This unwraps them.
|
||||
}
|
||||
|
||||
private class InMemoryTestHelper(private val application: Application) : SQLiteOpenHelper(application, null, null, 1) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@ import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
class DistributionListTablesTest {
|
||||
@@ -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,12 +2,11 @@ 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
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.deleteAll
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.withinTransaction
|
||||
@@ -33,8 +32,8 @@ class GroupTableTest {
|
||||
fun setUp() {
|
||||
groupTable = SignalDatabase.groups
|
||||
|
||||
groupTable.writableDatabase.delete(GroupTable.TABLE_NAME).run()
|
||||
groupTable.writableDatabase.delete(GroupTable.MembershipTable.TABLE_NAME).run()
|
||||
groupTable.writableDatabase.deleteAll(GroupTable.TABLE_NAME)
|
||||
groupTable.writableDatabase.deleteAll(GroupTable.MembershipTable.TABLE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -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()
|
||||
@@ -122,6 +106,37 @@ class GroupTableTest {
|
||||
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroup_whenIRemapRecipientsThatHaveAConflict_thenIExpectDeletion() {
|
||||
val v2Group = insertPushGroupWithSelfAndOthers(
|
||||
listOf(
|
||||
harness.others[0],
|
||||
harness.others[1]
|
||||
)
|
||||
)
|
||||
|
||||
insertThread(v2Group)
|
||||
|
||||
groupTable.remapRecipient(harness.others[0], harness.others[1])
|
||||
|
||||
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||
|
||||
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroup_whenIRemapRecipients_thenIExpectRemap() {
|
||||
val v2Group = insertPushGroup()
|
||||
insertThread(v2Group)
|
||||
|
||||
val newId = harness.others[1]
|
||||
groupTable.remapRecipient(harness.others[0], newId)
|
||||
|
||||
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||
|
||||
assertEquals(setOf(harness.self.id, newId), groupRecord.members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndMember_whenIIsCurrentMember_thenIExpectTrue() {
|
||||
val v2Group = insertPushGroup()
|
||||
@@ -150,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)
|
||||
@@ -242,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))
|
||||
@@ -261,25 +293,52 @@ class GroupTableTest {
|
||||
}
|
||||
|
||||
private fun insertPushGroup(
|
||||
title: String = "Test Group",
|
||||
members: List<DecryptedMember> = listOf(
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(harness.self.requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(harness.self.requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
.build(),
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(Recipient.resolved(harness.others[0]).requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(Recipient.resolved(harness.others[0]).requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
): GroupId {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(members)
|
||||
.setRevision(0)
|
||||
val decryptedGroupState = DecryptedGroup.Builder()
|
||||
.title(title)
|
||||
.members(members)
|
||||
.revision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
|
||||
}
|
||||
|
||||
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
|
||||
val selfMember: DecryptedMember = DecryptedMember.Builder()
|
||||
.aciBytes(harness.self.requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
.build()
|
||||
|
||||
val otherMembers: List<DecryptedMember> = others.map { id ->
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(Recipient.resolved(id).requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
.build()
|
||||
}
|
||||
|
||||
val decryptedGroupState = DecryptedGroup.Builder()
|
||||
.members(listOf(selfMember) + otherMembers)
|
||||
.revision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.assertNull
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireLongOrNull
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyPair
|
||||
import org.signal.libsignal.protocol.kem.KEMKeyType
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
|
||||
class KyberPreKeyTableTest {
|
||||
|
||||
private val aci: ACI = ACI.from(UUID.randomUUID())
|
||||
private val pni: PNI = PNI.from(UUID.randomUUID())
|
||||
|
||||
@Test
|
||||
fun markAllStaleIfNecessary_onlyUpdatesMatchingAccountAndZeroValues() {
|
||||
insertTestRecord(aci, id = 1)
|
||||
insertTestRecord(aci, id = 2)
|
||||
insertTestRecord(aci, id = 3, staleTime = 42)
|
||||
insertTestRecord(pni, id = 4)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.kyberPreKeys.markAllStaleIfNecessary(aci, now)
|
||||
|
||||
assertEquals(now, getStaleTime(aci, 1))
|
||||
assertEquals(now, getStaleTime(aci, 2))
|
||||
assertEquals(42L, getStaleTime(aci, 3))
|
||||
assertEquals(0L, getStaleTime(pni, 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_deleteOldBeforeThreshold() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10)
|
||||
insertTestRecord(aci, id = 2, staleTime = 10)
|
||||
insertTestRecord(aci, id = 3, staleTime = 10)
|
||||
insertTestRecord(aci, id = 4, staleTime = 15)
|
||||
insertTestRecord(aci, id = 5, staleTime = 0)
|
||||
|
||||
SignalDatabase.kyberPreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 0)
|
||||
|
||||
assertNull(getStaleTime(aci, 1))
|
||||
assertNull(getStaleTime(aci, 2))
|
||||
assertNull(getStaleTime(aci, 3))
|
||||
assertNotNull(getStaleTime(aci, 4))
|
||||
assertNotNull(getStaleTime(aci, 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_neverDeleteStaleOfZero() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 0)
|
||||
insertTestRecord(aci, id = 2, staleTime = 0)
|
||||
insertTestRecord(aci, id = 3, staleTime = 0)
|
||||
insertTestRecord(aci, id = 4, staleTime = 0)
|
||||
insertTestRecord(aci, id = 5, staleTime = 0)
|
||||
|
||||
SignalDatabase.kyberPreKeys.deleteAllStaleBefore(aci, threshold = 10, minCount = 1)
|
||||
|
||||
assertNotNull(getStaleTime(aci, 1))
|
||||
assertNotNull(getStaleTime(aci, 2))
|
||||
assertNotNull(getStaleTime(aci, 3))
|
||||
assertNotNull(getStaleTime(aci, 4))
|
||||
assertNotNull(getStaleTime(aci, 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_respectMinCount() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10)
|
||||
insertTestRecord(aci, id = 2, staleTime = 10)
|
||||
insertTestRecord(aci, id = 3, staleTime = 10)
|
||||
insertTestRecord(aci, id = 4, staleTime = 10)
|
||||
insertTestRecord(aci, id = 5, staleTime = 10)
|
||||
|
||||
SignalDatabase.kyberPreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 3)
|
||||
|
||||
assertNull(getStaleTime(aci, 1))
|
||||
assertNull(getStaleTime(aci, 2))
|
||||
assertNotNull(getStaleTime(aci, 3))
|
||||
assertNotNull(getStaleTime(aci, 4))
|
||||
assertNotNull(getStaleTime(aci, 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_respectAccount() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10)
|
||||
insertTestRecord(aci, id = 2, staleTime = 10)
|
||||
insertTestRecord(aci, id = 3, staleTime = 10)
|
||||
|
||||
insertTestRecord(pni, id = 4, staleTime = 10)
|
||||
insertTestRecord(pni, id = 5, staleTime = 10)
|
||||
|
||||
SignalDatabase.kyberPreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 2)
|
||||
|
||||
assertNull(getStaleTime(aci, 1))
|
||||
assertNotNull(getStaleTime(aci, 2))
|
||||
assertNotNull(getStaleTime(aci, 3))
|
||||
assertNotNull(getStaleTime(pni, 4))
|
||||
assertNotNull(getStaleTime(pni, 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_ignoreLastResortForMinCount() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10)
|
||||
insertTestRecord(aci, id = 2, staleTime = 10)
|
||||
insertTestRecord(aci, id = 3, staleTime = 10)
|
||||
insertTestRecord(aci, id = 4, staleTime = 10)
|
||||
insertTestRecord(aci, id = 5, staleTime = 10, lastResort = true)
|
||||
|
||||
SignalDatabase.kyberPreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 3)
|
||||
|
||||
assertNull(getStaleTime(aci, 1))
|
||||
assertNotNull(getStaleTime(aci, 2))
|
||||
assertNotNull(getStaleTime(aci, 3))
|
||||
assertNotNull(getStaleTime(aci, 4))
|
||||
assertNotNull(getStaleTime(aci, 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_neverDeleteLastResort() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10, lastResort = true)
|
||||
insertTestRecord(aci, id = 2, staleTime = 10, lastResort = true)
|
||||
insertTestRecord(aci, id = 3, staleTime = 10, lastResort = true)
|
||||
|
||||
SignalDatabase.oneTimePreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 0)
|
||||
|
||||
assertNotNull(getStaleTime(aci, 1))
|
||||
assertNotNull(getStaleTime(aci, 2))
|
||||
assertNotNull(getStaleTime(aci, 3))
|
||||
}
|
||||
|
||||
private fun insertTestRecord(account: ServiceId, id: Int, staleTime: Long = 0, lastResort: Boolean = false) {
|
||||
val kemKeyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024)
|
||||
SignalDatabase.kyberPreKeys.insert(
|
||||
serviceId = account,
|
||||
keyId = id,
|
||||
record = KyberPreKeyRecord(
|
||||
id,
|
||||
System.currentTimeMillis(),
|
||||
kemKeyPair,
|
||||
Curve.generateKeyPair().privateKey.calculateSignature(kemKeyPair.publicKey.serialize())
|
||||
),
|
||||
lastResort = lastResort
|
||||
)
|
||||
|
||||
val count = SignalDatabase.rawDatabase
|
||||
.update(KyberPreKeyTable.TABLE_NAME)
|
||||
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
|
||||
assertEquals(1, count)
|
||||
}
|
||||
|
||||
private fun getStaleTime(account: ServiceId, id: Int): Long? {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select(KyberPreKeyTable.STALE_TIMESTAMP)
|
||||
.from(KyberPreKeyTable.TABLE_NAME)
|
||||
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
|
||||
}
|
||||
|
||||
private fun ServiceId.toAccountId(): String {
|
||||
return when (this) {
|
||||
is ACI -> this.toString()
|
||||
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.forEach
|
||||
import org.signal.core.util.requireLong
|
||||
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.AppDependencies
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
|
||||
class LogDatabaseTest {
|
||||
|
||||
private val db: LogDatabase = LogDatabase.getInstance(AppDependencies.application)
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNamePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesMessagePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(messagePattern = "Message")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesStackTracePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(stackTracePattern = "stack")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameAndMessagePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", messagePattern = "Message")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameAndStackTracePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", stackTracePattern = "stack")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameAndMessageAndStackTracePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", messagePattern = "Message", stackTracePattern = "stack")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs true
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_doesNotMatchNamePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Blah")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameButNotMessagePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", messagePattern = "Blah")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNameButNotStackTracePattern() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test", stackTracePattern = "Blah")
|
||||
),
|
||||
promptThreshold = currentTime
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_matchesNamePatternButPromptedTooRecently() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
db.writableDatabase
|
||||
.updateAll(LogDatabase.CrashTable.TABLE_NAME)
|
||||
.values(LogDatabase.CrashTable.LAST_PROMPTED_AT to currentTime)
|
||||
.run()
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test")
|
||||
),
|
||||
promptThreshold = currentTime - 100
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_noMatches() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
val foundMatch = db.crashes.anyMatch(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test")
|
||||
),
|
||||
promptThreshold = currentTime - 100
|
||||
)
|
||||
|
||||
foundMatch assertIs false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun crashTable_updatesLastPromptTime() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "TestName",
|
||||
message = "Test Message",
|
||||
stackTrace = "test\nstack\ntrace"
|
||||
)
|
||||
|
||||
db.crashes.saveCrash(
|
||||
createdAt = currentTime,
|
||||
name = "XXX",
|
||||
message = "XXX",
|
||||
stackTrace = "XXX"
|
||||
)
|
||||
|
||||
db.crashes.markAsPrompted(
|
||||
listOf(
|
||||
CrashConfig.CrashPattern(namePattern = "Test")
|
||||
),
|
||||
promptedAt = currentTime
|
||||
)
|
||||
|
||||
db.writableDatabase
|
||||
.select(LogDatabase.CrashTable.NAME, LogDatabase.CrashTable.LAST_PROMPTED_AT)
|
||||
.from(LogDatabase.CrashTable.TABLE_NAME)
|
||||
.run()
|
||||
.forEach {
|
||||
if (it.requireNonNullString(LogDatabase.CrashTable.NAME) == "TestName") {
|
||||
it.requireLong(LogDatabase.CrashTable.LAST_PROMPTED_AT) assertIs currentTime
|
||||
} else {
|
||||
it.requireLong(LogDatabase.CrashTable.LAST_PROMPTED_AT) assertIs 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,8 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@@ -31,10 +30,10 @@ 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(ServiceId.from(UUID.randomUUID())) }
|
||||
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -49,7 +48,7 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
@@ -63,7 +62,7 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
|
||||
@@ -77,13 +76,13 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
@@ -97,13 +96,13 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
|
||||
@@ -116,18 +115,18 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 3,
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
@@ -141,18 +140,18 @@ class MessageTableTest_gifts {
|
||||
val messageId = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 3,
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
@@ -166,18 +165,18 @@ class MessageTableTest_gifts {
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 3,
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package org.thoughtcrime.securesms.database
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import java.util.Optional
|
||||
@@ -17,7 +17,6 @@ object MmsHelper {
|
||||
recipient: Recipient = Recipient.UNKNOWN,
|
||||
body: String = "body",
|
||||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
subscriptionId: Int = -1,
|
||||
expiresIn: Long = 0,
|
||||
viewOnce: Boolean = false,
|
||||
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
|
||||
@@ -32,7 +31,6 @@ object MmsHelper {
|
||||
recipient = recipient,
|
||||
body = body,
|
||||
timestamp = sentTimeMillis,
|
||||
subscriptionId = subscriptionId,
|
||||
expiresIn = expiresIn,
|
||||
viewOnce = viewOnce,
|
||||
distributionType = distributionType,
|
||||
@@ -57,9 +55,9 @@ object MmsHelper {
|
||||
}
|
||||
|
||||
fun insert(
|
||||
message: IncomingMediaMessage,
|
||||
message: IncomingMessage,
|
||||
threadId: Long
|
||||
): Optional<MessageTable.InsertResult> {
|
||||
return SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, threadId)
|
||||
return SignalDatabase.messages.insertMessageInbox(message, threadId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,11 @@ import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -41,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(ServiceId.from(UUID.randomUUID())) }
|
||||
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
|
||||
@@ -74,7 +73,8 @@ class MmsTableTest_stories {
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = sender,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
@@ -96,7 +96,8 @@ class MmsTableTest_stories {
|
||||
// GIVEN
|
||||
val sender = recipients[0]
|
||||
val messageId = MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = sender,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
@@ -123,7 +124,8 @@ class MmsTableTest_stories {
|
||||
// GIVEN
|
||||
val messageIds = recipients.take(5).map {
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = it,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
@@ -155,7 +157,8 @@ class MmsTableTest_stories {
|
||||
val unviewedIds: List<Long> = (0 until 5).map {
|
||||
Thread.sleep(5)
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = recipients[it],
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
serverTimeMillis = 2,
|
||||
@@ -169,7 +172,8 @@ class MmsTableTest_stories {
|
||||
val viewedIds: List<Long> = (0 until 5).map {
|
||||
Thread.sleep(5)
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = recipients[it],
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
serverTimeMillis = 2,
|
||||
@@ -214,7 +218,8 @@ class MmsTableTest_stories {
|
||||
fun givenNoOutgoingStories_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectFalse() {
|
||||
// GIVEN
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = recipients[0],
|
||||
sentTimeMillis = 200,
|
||||
serverTimeMillis = 2,
|
||||
@@ -253,8 +258,7 @@ class MmsTableTest_stories {
|
||||
val groupStoryId = MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = -1L
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
)
|
||||
|
||||
// WHEN
|
||||
@@ -319,19 +323,19 @@ class MmsTableTest_stories {
|
||||
val groupStoryId = MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = -1L
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = myStory.id,
|
||||
sentTimeMillis = 201,
|
||||
serverTimeMillis = 201,
|
||||
receivedTimeMillis = 202,
|
||||
parentStoryId = ParentStoryId.GroupReply(groupStoryId)
|
||||
),
|
||||
-1
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(myStory, ThreadTable.DistributionTypes.DEFAULT)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import junit.framework.TestCase.assertEquals
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.assertNull
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireLongOrNull
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
|
||||
class OneTimePreKeyTableTest {
|
||||
|
||||
private val aci: ACI = ACI.from(UUID.randomUUID())
|
||||
private val pni: PNI = PNI.from(UUID.randomUUID())
|
||||
|
||||
@Test
|
||||
fun markAllStaleIfNecessary_onlyUpdatesMatchingAccountAndZeroValues() {
|
||||
insertTestRecord(aci, id = 1)
|
||||
insertTestRecord(aci, id = 2)
|
||||
insertTestRecord(aci, id = 3, staleTime = 42)
|
||||
insertTestRecord(pni, id = 4)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.oneTimePreKeys.markAllStaleIfNecessary(aci, now)
|
||||
|
||||
assertEquals(now, getStaleTime(aci, 1))
|
||||
assertEquals(now, getStaleTime(aci, 2))
|
||||
assertEquals(42L, getStaleTime(aci, 3))
|
||||
assertEquals(0L, getStaleTime(pni, 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_deleteOldBeforeThreshold() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10)
|
||||
insertTestRecord(aci, id = 2, staleTime = 10)
|
||||
insertTestRecord(aci, id = 3, staleTime = 10)
|
||||
insertTestRecord(aci, id = 4, staleTime = 15)
|
||||
insertTestRecord(aci, id = 5, staleTime = 0)
|
||||
|
||||
SignalDatabase.oneTimePreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 0)
|
||||
|
||||
assertNull(getStaleTime(aci, 1))
|
||||
assertNull(getStaleTime(aci, 2))
|
||||
assertNull(getStaleTime(aci, 3))
|
||||
assertNotNull(getStaleTime(aci, 4))
|
||||
assertNotNull(getStaleTime(aci, 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_neverDeleteStaleOfZero() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 0)
|
||||
insertTestRecord(aci, id = 2, staleTime = 0)
|
||||
insertTestRecord(aci, id = 3, staleTime = 0)
|
||||
insertTestRecord(aci, id = 4, staleTime = 0)
|
||||
insertTestRecord(aci, id = 5, staleTime = 0)
|
||||
|
||||
SignalDatabase.oneTimePreKeys.deleteAllStaleBefore(aci, threshold = 10, minCount = 0)
|
||||
|
||||
assertNotNull(getStaleTime(aci, 1))
|
||||
assertNotNull(getStaleTime(aci, 2))
|
||||
assertNotNull(getStaleTime(aci, 3))
|
||||
assertNotNull(getStaleTime(aci, 4))
|
||||
assertNotNull(getStaleTime(aci, 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_respectMinCount() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10)
|
||||
insertTestRecord(aci, id = 2, staleTime = 10)
|
||||
insertTestRecord(aci, id = 3, staleTime = 10)
|
||||
insertTestRecord(aci, id = 4, staleTime = 10)
|
||||
insertTestRecord(aci, id = 5, staleTime = 10)
|
||||
|
||||
SignalDatabase.oneTimePreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 3)
|
||||
|
||||
assertNull(getStaleTime(aci, 1))
|
||||
assertNull(getStaleTime(aci, 2))
|
||||
assertNotNull(getStaleTime(aci, 3))
|
||||
assertNotNull(getStaleTime(aci, 4))
|
||||
assertNotNull(getStaleTime(aci, 5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteAllStaleBefore_respectAccount() {
|
||||
insertTestRecord(aci, id = 1, staleTime = 10)
|
||||
insertTestRecord(aci, id = 2, staleTime = 10)
|
||||
insertTestRecord(aci, id = 3, staleTime = 10)
|
||||
|
||||
insertTestRecord(pni, id = 4, staleTime = 10)
|
||||
insertTestRecord(pni, id = 5, staleTime = 10)
|
||||
|
||||
SignalDatabase.oneTimePreKeys.deleteAllStaleBefore(aci, threshold = 11, minCount = 2)
|
||||
|
||||
assertNull(getStaleTime(aci, 1))
|
||||
assertNotNull(getStaleTime(aci, 2))
|
||||
assertNotNull(getStaleTime(aci, 3))
|
||||
assertNotNull(getStaleTime(pni, 4))
|
||||
assertNotNull(getStaleTime(pni, 5))
|
||||
}
|
||||
|
||||
private fun insertTestRecord(account: ServiceId, id: Int, staleTime: Long = 0) {
|
||||
SignalDatabase.oneTimePreKeys.insert(
|
||||
serviceId = account,
|
||||
keyId = id,
|
||||
record = PreKeyRecord(id, Curve.generateKeyPair())
|
||||
)
|
||||
|
||||
val count = SignalDatabase.rawDatabase
|
||||
.update(OneTimePreKeyTable.TABLE_NAME)
|
||||
.values(OneTimePreKeyTable.STALE_TIMESTAMP to staleTime)
|
||||
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
|
||||
.run()
|
||||
|
||||
assertEquals(1, count)
|
||||
}
|
||||
|
||||
private fun getStaleTime(account: ServiceId, id: Int): Long? {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select(OneTimePreKeyTable.STALE_TIMESTAMP)
|
||||
.from(OneTimePreKeyTable.TABLE_NAME)
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,8 @@ import org.signal.core.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -24,14 +22,14 @@ class RecipientTableTest {
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun givenAHiddenRecipient_whenIQueryAllContacts_thenIDoNotExpectHiddenToBeReturned() {
|
||||
fun givenAHiddenRecipient_whenIQueryAllContacts_thenIExpectHiddenToBeReturned() {
|
||||
val hiddenRecipient = harness.others[0]
|
||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||
|
||||
val results = SignalDatabase.recipients.queryAllContacts("Hidden")!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
assertEquals(1, results.count)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -59,7 +57,7 @@ class RecipientTableTest {
|
||||
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
|
||||
SignalDatabase.recipients.markHidden(hiddenRecipient)
|
||||
|
||||
val results = SignalDatabase.recipients.querySignalContacts("Hidden", false)!!
|
||||
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Hidden", false))!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
@@ -130,7 +128,7 @@ class RecipientTableTest {
|
||||
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
|
||||
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
|
||||
|
||||
val results = SignalDatabase.recipients.querySignalContacts("Blocked", false)!!
|
||||
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Blocked", false))!!
|
||||
|
||||
assertEquals(0, results.count)
|
||||
}
|
||||
@@ -167,16 +165,14 @@ class RecipientTableTest {
|
||||
|
||||
@Test
|
||||
fun givenARecipientWithPniAndAci_whenIMarkItUnregistered_thenIExpectItToBeSplit() {
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
|
||||
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||
|
||||
SignalDatabase.recipients.markUnregistered(mainId)
|
||||
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
assertEquals(mainId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
@@ -185,17 +181,15 @@ class RecipientTableTest {
|
||||
|
||||
@Test
|
||||
fun givenARecipientWithPniAndAci_whenISplitItForStorageSync_thenIExpectItToBeSplit() {
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
|
||||
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||
val mainRecord = SignalDatabase.recipients.getRecord(mainId)
|
||||
|
||||
SignalDatabase.recipients.splitForStorageSync(mainRecord.storageId!!)
|
||||
SignalDatabase.recipients.splitForStorageSyncIfNecessary(mainRecord.aci!!)
|
||||
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
assertEquals(mainId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2023 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.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientTableTest_applyStorageSyncContactUpdate {
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun insertMessageOnVerifiedToDefault() {
|
||||
// GIVEN
|
||||
val identities = AppDependencies.protocolStore.aci().identities()
|
||||
val other = Recipient.resolved(harness.others[0])
|
||||
|
||||
MmsHelper.insert(recipient = other)
|
||||
identities.setVerified(other.id, harness.othersKeys[0].publicKey, IdentityTable.VerifiedStatus.VERIFIED)
|
||||
|
||||
val oldRecord: SignalContactRecord = StorageSyncModels.localToRemoteRecord(SignalDatabase.recipients.getRecordForSync(harness.others[0])!!).contact.get()
|
||||
|
||||
val newProto = oldRecord
|
||||
.toProto()
|
||||
.newBuilder()
|
||||
.identityState(ContactRecord.IdentityState.DEFAULT)
|
||||
.build()
|
||||
val newRecord = SignalContactRecord(oldRecord.id, newProto)
|
||||
|
||||
val update = StorageRecordUpdate<SignalContactRecord>(oldRecord, newRecord)
|
||||
|
||||
// WHEN
|
||||
val oldVerifiedStatus: IdentityTable.VerifiedStatus = identities.getIdentityRecord(other.id).get().verifiedStatus
|
||||
SignalDatabase.recipients.applyStorageSyncContactUpdate(update)
|
||||
val newVerifiedStatus: IdentityTable.VerifiedStatus = identities.getIdentityRecord(other.id).get().verifiedStatus
|
||||
|
||||
// THEN
|
||||
oldVerifiedStatus assertIs IdentityTable.VerifiedStatus.VERIFIED
|
||||
newVerifiedStatus assertIs IdentityTable.VerifiedStatus.DEFAULT
|
||||
|
||||
val messages = MessageTableTestUtils.getMessages(SignalDatabase.threads.getThreadIdFor(other.id)!!)
|
||||
messages.first().isIdentityDefault assertIs true
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,18 @@ import org.hamcrest.MatcherAssert
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.exists
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.core.util.readToSingleBoolean
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.select
|
||||
@@ -29,33 +34,28 @@ 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.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
SignalStore.account().setE164(E164_SELF)
|
||||
SignalStore.account().setAci(ACI_SELF)
|
||||
SignalStore.account().setPni(PNI_SELF)
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
SignalStore.account.setE164(E164_SELF)
|
||||
SignalStore.account.setAci(ACI_SELF)
|
||||
SignalStore.account.setPni(PNI_SELF)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -83,6 +83,42 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
val record = SignalDatabase.recipients.getRecord(id)
|
||||
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
|
||||
}
|
||||
|
||||
test("e164+pni insert") {
|
||||
val id = process(E164_A, PNI_A, null)
|
||||
expect(E164_A, PNI_A, null)
|
||||
|
||||
val record = SignalDatabase.recipients.getRecord(id)
|
||||
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
|
||||
}
|
||||
|
||||
test("e164+aci insert") {
|
||||
val id = process(E164_A, null, ACI_A)
|
||||
expect(E164_A, null, ACI_A)
|
||||
|
||||
val record = SignalDatabase.recipients.getRecord(id)
|
||||
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
|
||||
}
|
||||
|
||||
test("e164+pni+aci insert") {
|
||||
val id = process(E164_A, PNI_A, ACI_A)
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
val record = SignalDatabase.recipients.getRecord(id)
|
||||
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
|
||||
}
|
||||
|
||||
test("e164+pni+aci insert, pni verified") {
|
||||
val id = process(E164_A, PNI_A, ACI_A, pniVerified = true)
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
expectPniVerified()
|
||||
|
||||
val record = SignalDatabase.recipients.getRecord(id)
|
||||
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
|
||||
|
||||
process(E164_A, PNI_A, ACI_A, pniVerified = false)
|
||||
expectPniVerified()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,9 +128,9 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expect(E164_A, null, null)
|
||||
}
|
||||
|
||||
test("no match, e164 and pni") {
|
||||
process(E164_A, PNI_A, null)
|
||||
expect(E164_A, PNI_A, null)
|
||||
test("no match, pni-only") {
|
||||
process(null, PNI_A, null)
|
||||
expect(null, PNI_A, null)
|
||||
}
|
||||
|
||||
test("no match, aci-only") {
|
||||
@@ -102,6 +138,11 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expect(null, null, ACI_A)
|
||||
}
|
||||
|
||||
test("no match, e164 and pni") {
|
||||
process(E164_A, PNI_A, null)
|
||||
expect(E164_A, PNI_A, null)
|
||||
}
|
||||
|
||||
test("no match, e164 and aci") {
|
||||
process(E164_A, null, ACI_A)
|
||||
expect(E164_A, null, ACI_A)
|
||||
@@ -111,6 +152,31 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
process(null, null, null)
|
||||
}
|
||||
|
||||
test("pni matches, pni+aci provided, no pni session") {
|
||||
given(E164_A, PNI_A, null)
|
||||
process(null, PNI_A, ACI_A)
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
expectNoSessionSwitchoverEvent()
|
||||
}
|
||||
|
||||
test("pni matches, pni+aci provided, pni session") {
|
||||
given(E164_A, PNI_A, null, pniSession = true)
|
||||
process(null, PNI_A, ACI_A)
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
expectSessionSwitchoverEvent(E164_A)
|
||||
}
|
||||
|
||||
test("pni matches, pni+aci provided, pni session, pni-verified") {
|
||||
given(E164_A, PNI_A, null, pniSession = true)
|
||||
process(null, PNI_A, ACI_A, pniVerified = true)
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
expectNoSessionSwitchoverEvent()
|
||||
expectPniVerified()
|
||||
}
|
||||
|
||||
test("no match, all fields") {
|
||||
process(E164_A, PNI_A, ACI_A)
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
@@ -166,6 +232,14 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectSessionSwitchoverEvent(E164_A)
|
||||
}
|
||||
|
||||
test("e164 and pni matches, all provided, new aci, existing pni session, pni-verified") {
|
||||
given(E164_A, PNI_A, null, pniSession = true)
|
||||
process(E164_A, PNI_A, ACI_A, pniVerified = true)
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
expectPniVerified()
|
||||
}
|
||||
|
||||
test("e164 and aci matches, all provided, new pni") {
|
||||
given(E164_A, null, ACI_A)
|
||||
process(E164_A, PNI_A, ACI_A)
|
||||
@@ -308,6 +382,26 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectChangeNumberEvent()
|
||||
}
|
||||
|
||||
test("steal, pni is changed") {
|
||||
given(E164_A, PNI_B, ACI_A)
|
||||
given(E164_B, PNI_A, null)
|
||||
|
||||
process(E164_A, PNI_A, null)
|
||||
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
expect(E164_B, null, null)
|
||||
}
|
||||
|
||||
test("steal, pni is changed, aci left behind") {
|
||||
given(E164_B, PNI_A, ACI_A)
|
||||
given(E164_A, PNI_B, null)
|
||||
|
||||
process(E164_A, PNI_A, null)
|
||||
|
||||
expect(E164_B, null, ACI_A)
|
||||
expect(E164_A, PNI_A, null)
|
||||
}
|
||||
|
||||
test("steal, e164+pni & e164+pni, no aci provided, no pni session") {
|
||||
given(E164_A, PNI_B, null)
|
||||
given(E164_B, PNI_A, null)
|
||||
@@ -352,7 +446,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectChangeNumberEvent()
|
||||
}
|
||||
|
||||
test("steal, e164 & pni+e164, no aci provided") {
|
||||
test("steal, e164 & pni+e164, no aci provided, pni session exists") {
|
||||
val id1 = given(E164_A, null, null)
|
||||
val id2 = given(E164_B, PNI_A, null, pniSession = true)
|
||||
|
||||
@@ -365,6 +459,16 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectSessionSwitchoverEvent(id2, E164_B)
|
||||
}
|
||||
|
||||
test("steal, e164 & pni+e164, no aci provided, no pni session") {
|
||||
given(E164_A, null, null)
|
||||
given(E164_B, PNI_A, null)
|
||||
|
||||
process(E164_A, PNI_A, null)
|
||||
|
||||
expect(E164_A, PNI_A, null)
|
||||
expect(E164_B, null, null)
|
||||
}
|
||||
|
||||
test("steal, e164+pni+aci & e164+aci, no pni provided, change number") {
|
||||
given(E164_A, PNI_A, ACI_A)
|
||||
given(E164_B, null, ACI_B)
|
||||
@@ -377,6 +481,76 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectChangeNumberEvent()
|
||||
}
|
||||
|
||||
test("steal, e164+aci & aci, no pni provided, existing aci session") {
|
||||
given(E164_A, null, ACI_A, aciSession = true)
|
||||
given(null, null, ACI_B)
|
||||
|
||||
process(E164_A, null, ACI_B)
|
||||
|
||||
expect(null, null, ACI_A)
|
||||
expect(E164_A, null, ACI_B)
|
||||
|
||||
expectNoSessionSwitchoverEvent()
|
||||
}
|
||||
|
||||
test("steal, e164+pni+aci & aci, no pni provided, existing aci session") {
|
||||
given(E164_A, PNI_A, ACI_A, aciSession = true)
|
||||
given(null, null, ACI_B)
|
||||
|
||||
process(E164_A, null, ACI_B)
|
||||
|
||||
expect(null, PNI_A, ACI_A)
|
||||
expect(E164_A, null, ACI_B)
|
||||
|
||||
expectNoSessionSwitchoverEvent()
|
||||
}
|
||||
|
||||
test("steal, e164+pni+aci & aci, no pni provided, existing pni session") {
|
||||
given(E164_A, PNI_A, ACI_A, pniSession = true)
|
||||
given(null, null, ACI_B)
|
||||
|
||||
process(E164_A, null, ACI_B)
|
||||
|
||||
expect(null, PNI_A, ACI_A)
|
||||
expect(E164_A, null, ACI_B)
|
||||
|
||||
expectNoSessionSwitchoverEvent()
|
||||
}
|
||||
|
||||
test("steal, e164+pni & aci, no pni provided, no pni session") {
|
||||
given(E164_A, PNI_A, null)
|
||||
given(null, null, ACI_A)
|
||||
|
||||
process(E164_A, null, ACI_A)
|
||||
|
||||
expect(null, PNI_A, null)
|
||||
expect(E164_A, null, ACI_A)
|
||||
}
|
||||
|
||||
test("steal, e164+pni & aci, no pni provided, pni session") {
|
||||
given(E164_A, PNI_A, null, pniSession = true)
|
||||
given(null, null, ACI_A)
|
||||
|
||||
process(E164_A, null, ACI_A)
|
||||
|
||||
expect(null, PNI_A, null)
|
||||
expect(E164_A, null, ACI_A)
|
||||
|
||||
expectNoSessionSwitchoverEvent()
|
||||
}
|
||||
|
||||
test("steal, e164+pni+aci * pni+aci, all provided, aci sessions but not pni sessions, no SSE expected") {
|
||||
given(E164_A, PNI_A, ACI_A, createThread = true, aciSession = true, pniSession = false)
|
||||
given(null, PNI_B, ACI_B, createThread = false, aciSession = true, pniSession = false)
|
||||
|
||||
process(E164_A, PNI_B, ACI_A)
|
||||
|
||||
expect(E164_A, PNI_B, ACI_A)
|
||||
expect(null, null, ACI_B)
|
||||
|
||||
expectNoSessionSwitchoverEvent()
|
||||
}
|
||||
|
||||
test("merge, e164 & pni & aci, all provided") {
|
||||
given(E164_A, null, null)
|
||||
given(null, PNI_A, null)
|
||||
@@ -501,7 +675,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectThreadMergeEvent(E164_A)
|
||||
}
|
||||
|
||||
test("merge, e164+pni & aci, pni session, thread merge shadows") {
|
||||
test("merge, e164+pni & aci, pni session, thread merge shadows SSE") {
|
||||
given(E164_A, PNI_A, null, pniSession = true)
|
||||
given(null, null, ACI_A)
|
||||
|
||||
@@ -533,6 +707,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
expectDeleted()
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
expectPniVerified()
|
||||
}
|
||||
|
||||
test("merge, e164+pni & aci, pni session, pni verified") {
|
||||
@@ -545,6 +721,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
expectThreadMergeEvent(E164_A)
|
||||
expectPniVerified()
|
||||
}
|
||||
|
||||
test("merge, e164+pni & e164+pni+aci, change number") {
|
||||
@@ -599,6 +776,41 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
expectThreadMergeEvent(E164_A)
|
||||
}
|
||||
|
||||
test("merge, e164+pni & e164+aci, pni+aci provided, change number") {
|
||||
given(E164_A, PNI_A, null)
|
||||
given(E164_B, null, ACI_A)
|
||||
|
||||
process(null, PNI_A, ACI_A)
|
||||
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
expectThreadMergeEvent(E164_A)
|
||||
expectChangeNumberEvent()
|
||||
}
|
||||
|
||||
test("merge, e164 + pni reassigned, aci abandoned") {
|
||||
given(E164_A, PNI_A, ACI_A)
|
||||
given(E164_B, PNI_B, ACI_B)
|
||||
|
||||
process(E164_A, PNI_A, ACI_B)
|
||||
|
||||
expect(null, null, ACI_A)
|
||||
expect(E164_A, PNI_A, ACI_B)
|
||||
|
||||
expectChangeNumberEvent()
|
||||
}
|
||||
|
||||
test("merge, e164 follows pni+aci") {
|
||||
given(E164_A, PNI_A, null)
|
||||
given(null, null, ACI_A)
|
||||
|
||||
process(null, PNI_A, ACI_A, pniVerified = true)
|
||||
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
expectThreadMergeEvent(E164_A)
|
||||
expectPniVerified()
|
||||
}
|
||||
|
||||
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
|
||||
given(E164_SELF, null, ACI_SELF)
|
||||
given(null, null, ACI_A)
|
||||
@@ -620,6 +832,22 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
process(E164_A, null, ACI_SELF, changeSelf = true)
|
||||
expect(E164_A, null, ACI_SELF)
|
||||
}
|
||||
|
||||
test("local user, local e164+aci provided, changeSelf=false, leave pni alone") {
|
||||
given(E164_SELF, PNI_SELF, ACI_SELF)
|
||||
|
||||
process(E164_SELF, PNI_A, ACI_A)
|
||||
|
||||
expect(E164_SELF, PNI_SELF, ACI_SELF)
|
||||
}
|
||||
|
||||
test("local user, local e164+aci provided, changeSelf=false, leave pni alone") {
|
||||
given(E164_SELF, PNI_A, ACI_SELF)
|
||||
|
||||
process(E164_SELF, PNI_SELF, ACI_A)
|
||||
|
||||
expect(E164_SELF, PNI_A, ACI_SELF)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -636,9 +864,9 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
val smsId2: Long = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
|
||||
val smsId3: Long = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
|
||||
|
||||
val mmsId1: Long = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
|
||||
val mmsId2: Long = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
|
||||
val mmsId3: Long = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
|
||||
val mmsId1: Long = SignalDatabase.messages.insertMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
|
||||
val mmsId2: Long = SignalDatabase.messages.insertMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
|
||||
val mmsId3: Long = SignalDatabase.messages.insertMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
|
||||
|
||||
val threadIdAci: Long = SignalDatabase.threads.getThreadIdFor(recipientIdAci)!!
|
||||
val threadIdE164: Long = SignalDatabase.threads.getThreadIdFor(recipientIdE164)!!
|
||||
@@ -685,8 +913,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
// Thread validation
|
||||
assertEquals(threadIdAci, retrievedThreadId)
|
||||
Assert.assertNull(SignalDatabase.threads.getThreadIdFor(recipientIdE164))
|
||||
Assert.assertNull(SignalDatabase.threads.getThreadRecord(threadIdE164))
|
||||
assertNull(SignalDatabase.threads.getThreadIdFor(recipientIdE164))
|
||||
assertNull(SignalDatabase.threads.getThreadRecord(threadIdE164))
|
||||
|
||||
// SMS validation
|
||||
val sms1: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId1)!!
|
||||
@@ -730,10 +958,10 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
// Identity validation
|
||||
assertEquals(identityKeyAci, SignalDatabase.identities.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
|
||||
Assert.assertNull(SignalDatabase.identities.getIdentityStoreRecord(E164_A))
|
||||
assertNull(SignalDatabase.identities.getIdentityStoreRecord(E164_A))
|
||||
|
||||
// Session validation
|
||||
Assert.assertNotNull(SignalDatabase.sessions.load(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1)))
|
||||
assertNotNull(SignalDatabase.sessions.load(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1)))
|
||||
|
||||
// Reaction validation
|
||||
val reactionsSms: List<ReactionRecord> = SignalDatabase.reactions.getReactions(MessageId(smsId1))
|
||||
@@ -758,18 +986,42 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
|
||||
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
|
||||
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMessage {
|
||||
return IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = sender,
|
||||
sentTimeMillis = time,
|
||||
serverTimeMillis = time,
|
||||
receivedTimeMillis = time,
|
||||
body = body,
|
||||
groupId = groupId.orNull(),
|
||||
isUnidentified = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
|
||||
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty(), false, false)
|
||||
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMessage {
|
||||
return IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = sender,
|
||||
groupId = groupId.orNull(),
|
||||
body = body,
|
||||
sentTimeMillis = time,
|
||||
receivedTimeMillis = time,
|
||||
serverTimeMillis = time,
|
||||
isUnidentified = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun identityKey(value: Byte): IdentityKey {
|
||||
val byteArray = ByteArray(32)
|
||||
byteArray[0] = value
|
||||
return identityKey(byteArray)
|
||||
}
|
||||
|
||||
private fun identityKey(value: ByteArray): IdentityKey {
|
||||
val bytes = ByteArray(33)
|
||||
bytes[0] = 0x05
|
||||
bytes[1] = value
|
||||
value.copyInto(bytes, 1)
|
||||
return IdentityKey(bytes)
|
||||
}
|
||||
|
||||
@@ -824,6 +1076,10 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
if (!test.sessionSwitchoverExpected) {
|
||||
test.expectNoSessionSwitchoverEvent()
|
||||
}
|
||||
|
||||
if (!test.pniVerifiedExpected) {
|
||||
test.expectPniNotVerified()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e.javaClass != exception) {
|
||||
val error = java.lang.AssertionError("[$name] ${e.message}")
|
||||
@@ -843,11 +1099,12 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
var changeNumberExpected = false
|
||||
var threadMergeExpected = false
|
||||
var sessionSwitchoverExpected = false
|
||||
var pniVerifiedExpected = false
|
||||
|
||||
init {
|
||||
// Need to delete these first to prevent foreign key crash
|
||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list")
|
||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list_member")
|
||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.ListTable.TABLE_NAME}")
|
||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.MembershipTable.TABLE_NAME}")
|
||||
|
||||
SqlUtil.getAllTables(SignalDatabase.rawDatabase)
|
||||
.filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB
|
||||
@@ -856,7 +1113,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
|
||||
}
|
||||
|
||||
ApplicationDependencies.getRecipientCache().clear()
|
||||
AppDependencies.recipientCache.clear()
|
||||
AppDependencies.recipientCache.clearSelf()
|
||||
RecipientId.clearCache()
|
||||
}
|
||||
|
||||
@@ -865,14 +1123,15 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
pni: PNI?,
|
||||
aci: ACI?,
|
||||
createThread: Boolean = true,
|
||||
pniSession: Boolean = false
|
||||
pniSession: Boolean = false,
|
||||
aciSession: Boolean = false
|
||||
): RecipientId {
|
||||
val id = insert(e164, pni, aci)
|
||||
generatedIds += id
|
||||
if (createThread) {
|
||||
// Create a thread and throw a dummy message in it so it doesn't get automatically deleted
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(id))
|
||||
SignalDatabase.messages.insertMessageInbox(IncomingEncryptedMessage(IncomingTextMessage(id, 1, 0, 0, 0, "", Optional.empty(), 0, false, ""), ""))
|
||||
val result = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = id, time = (Math.random() * 10000000).toLong(), body = "1"))
|
||||
SignalDatabase.threads.markAsActiveEarly(result.get().threadId)
|
||||
}
|
||||
|
||||
if (pniSession) {
|
||||
@@ -883,11 +1142,42 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
SignalDatabase.sessions.store(pni, SignalProtocolAddress(pni.toString(), 1), SessionRecord())
|
||||
}
|
||||
|
||||
if (aciSession) {
|
||||
if (aci == null) {
|
||||
throw IllegalArgumentException("aciSession = true but aci is null!")
|
||||
}
|
||||
|
||||
SignalDatabase.sessions.store(aci, SignalProtocolAddress(aci.toString(), 1), SessionRecord())
|
||||
}
|
||||
|
||||
if (aci != null) {
|
||||
SignalDatabase.identities.saveIdentity(
|
||||
addressName = aci.toString(),
|
||||
recipientId = id,
|
||||
identityKey = identityKey(Util.getSecretBytes(32)),
|
||||
verifiedStatus = IdentityTable.VerifiedStatus.DEFAULT,
|
||||
firstUse = true,
|
||||
timestamp = 0,
|
||||
nonBlockingApproval = false
|
||||
)
|
||||
}
|
||||
if (pni != null) {
|
||||
SignalDatabase.identities.saveIdentity(
|
||||
addressName = pni.toString(),
|
||||
recipientId = id,
|
||||
identityKey = identityKey(Util.getSecretBytes(32)),
|
||||
verifiedStatus = IdentityTable.VerifiedStatus.DEFAULT,
|
||||
firstUse = true,
|
||||
timestamp = 0,
|
||||
nonBlockingApproval = false
|
||||
)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
fun process(e164: String?, pni: PNI?, aci: ACI?, changeSelf: Boolean = false, pniVerified: Boolean = false): RecipientId {
|
||||
outputRecipientId = SignalDatabase.recipients.getAndPossiblyMerge(serviceId = aci ?: pni, pni = pni, e164 = e164, pniVerified = pniVerified, changeSelf = changeSelf)
|
||||
outputRecipientId = SignalDatabase.recipients.getAndPossiblyMerge(aci = aci, pni = pni, e164 = e164, pniVerified = pniVerified, changeSelf = changeSelf)
|
||||
generatedIds += outputRecipientId
|
||||
return outputRecipientId
|
||||
}
|
||||
@@ -901,15 +1191,15 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
val expected = RecipientTuple(
|
||||
e164 = e164,
|
||||
pni = pni,
|
||||
serviceId = aci ?: pni
|
||||
aci = aci
|
||||
)
|
||||
val actual = RecipientTuple(
|
||||
e164 = recipient.e164.orElse(null),
|
||||
pni = recipient.pni.orElse(null),
|
||||
serviceId = recipient.serviceId.orElse(null)
|
||||
aci = recipient.aci.orElse(null)
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
assertEquals("Recipient $id did not match expected result!", expected, actual)
|
||||
}
|
||||
|
||||
fun expectDeleted() {
|
||||
@@ -917,21 +1207,21 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
}
|
||||
|
||||
fun expectDeleted(id: RecipientId) {
|
||||
SignalDatabase.rawDatabase
|
||||
.select("1")
|
||||
.from(RecipientTable.TABLE_NAME)
|
||||
val found = SignalDatabase.rawDatabase
|
||||
.exists(RecipientTable.TABLE_NAME)
|
||||
.where("${RecipientTable.ID} = ?", id)
|
||||
.run()
|
||||
.use { !it.moveToFirst() }
|
||||
|
||||
assertFalse("Expected $id to be deleted, but it's still present!", found)
|
||||
}
|
||||
|
||||
fun expectChangeNumberEvent() {
|
||||
assertEquals(1, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
|
||||
assertEquals("Missing change number event!", 1, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
|
||||
changeNumberExpected = true
|
||||
}
|
||||
|
||||
fun expectNoChangeNumberEvent() {
|
||||
assertEquals(0, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
|
||||
assertEquals("Unexpected change number event!", 0, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
|
||||
changeNumberExpected = false
|
||||
}
|
||||
|
||||
@@ -941,42 +1231,57 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
fun expectSessionSwitchoverEvent(recipientId: RecipientId, e164: String) {
|
||||
val event: SessionSwitchoverEvent? = getLatestSessionSwitchoverEvent(recipientId)
|
||||
assertNotNull(event)
|
||||
assertNotNull("Missing session switchover event! Expected one with e164 = $e164", event)
|
||||
assertEquals(e164, event!!.e164)
|
||||
sessionSwitchoverExpected = true
|
||||
}
|
||||
|
||||
fun expectNoSessionSwitchoverEvent() {
|
||||
assertNull(getLatestSessionSwitchoverEvent(outputRecipientId))
|
||||
assertNull("Unexpected session switchover event!", getLatestSessionSwitchoverEvent(outputRecipientId))
|
||||
}
|
||||
|
||||
fun expectThreadMergeEvent(previousE164: String) {
|
||||
val event: ThreadMergeEvent? = getLatestThreadMergeEvent(outputRecipientId)
|
||||
assertNotNull(event)
|
||||
assertEquals(previousE164, event!!.previousE164)
|
||||
assertNotNull("Missing thread merge event! Expected one with e164 = $previousE164", event)
|
||||
assertEquals("E164 on thread merge event doesn't match!", previousE164, event!!.previousE164)
|
||||
threadMergeExpected = true
|
||||
}
|
||||
|
||||
fun expectNoThreadMergeEvent() {
|
||||
assertNull(getLatestThreadMergeEvent(outputRecipientId))
|
||||
assertNull("Unexpected thread merge event!", getLatestThreadMergeEvent(outputRecipientId))
|
||||
}
|
||||
|
||||
fun expectPniVerified() {
|
||||
assertTrue("Expected PNI to be verified!", isPniVerified(outputRecipientId))
|
||||
pniVerifiedExpected = true
|
||||
}
|
||||
|
||||
fun expectPniNotVerified() {
|
||||
assertFalse("Expected PNI to be not be verified!", isPniVerified(outputRecipientId))
|
||||
}
|
||||
|
||||
private fun isPniVerified(recipientId: RecipientId): Boolean {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select(RecipientTable.PNI_SIGNATURE_VERIFIED)
|
||||
.from(RecipientTable.TABLE_NAME)
|
||||
.where("${RecipientTable.ID} = ?", recipientId)
|
||||
.run()
|
||||
.readToSingleBoolean(false)
|
||||
}
|
||||
|
||||
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
|
||||
val serviceIdString: String? = (aci ?: pni)?.toString()
|
||||
val pniString: String? = pni?.toString()
|
||||
|
||||
val id: Long = SignalDatabase.rawDatabase.insert(
|
||||
RecipientTable.TABLE_NAME,
|
||||
null,
|
||||
contentValuesOf(
|
||||
RecipientTable.PHONE to e164,
|
||||
RecipientTable.SERVICE_ID to serviceIdString,
|
||||
RecipientTable.PNI_COLUMN to pniString,
|
||||
RecipientTable.E164 to e164,
|
||||
RecipientTable.ACI_COLUMN to aci?.toString(),
|
||||
RecipientTable.PNI_COLUMN to pni?.toString(),
|
||||
RecipientTable.REGISTERED to RecipientTable.RegisteredState.REGISTERED.id
|
||||
)
|
||||
)
|
||||
|
||||
assertTrue("Failed to insert! E164: $e164, ServiceId: $serviceIdString, PNI: $pniString", id > 0)
|
||||
assertTrue("Failed to insert! E164: $e164, ACI: $aci, PNI: $pni", id > 0)
|
||||
|
||||
return RecipientId.from(id)
|
||||
}
|
||||
@@ -985,14 +1290,14 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
data class RecipientTuple(
|
||||
val e164: String?,
|
||||
val pni: PNI?,
|
||||
val serviceId: ServiceId?
|
||||
val aci: ACI?
|
||||
) {
|
||||
|
||||
/**
|
||||
* The intent here is to give nice diffs with the name of the constants rather than the values.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return "(${e164.e164String()}, ${pni.pniString()}, ${serviceId.serviceIdString()})"
|
||||
return "(${e164.e164String()}, ${pni.pniString()}, ${aci.aciString()})"
|
||||
}
|
||||
|
||||
private fun String?.e164String(): String {
|
||||
@@ -1016,12 +1321,9 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
} ?: "null"
|
||||
}
|
||||
|
||||
private fun ServiceId?.serviceIdString(): String {
|
||||
private fun ACI?.aciString(): String {
|
||||
return this?.let {
|
||||
when (it) {
|
||||
PNI_A -> "PNI_A"
|
||||
PNI_B -> "PNI_B"
|
||||
PNI_SELF -> "PNI_SELF"
|
||||
ACI_A -> "ACI_A"
|
||||
ACI_B -> "ACI_B"
|
||||
ACI_SELF -> "ACI_SELF"
|
||||
@@ -1042,7 +1344,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
.use { cursor: Cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
|
||||
ThreadMergeEvent.parseFrom(bytes)
|
||||
ThreadMergeEvent.ADAPTER.decode(bytes)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -1060,7 +1362,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
.use { cursor: Cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
|
||||
SessionSwitchoverEvent.parseFrom(bytes)
|
||||
SessionSwitchoverEvent.ADAPTER.decode(bytes)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.testing.SignalFlakyTestRule
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
@@ -18,6 +20,9 @@ class SQLiteDatabaseTest {
|
||||
|
||||
private lateinit var db: SQLiteDatabase
|
||||
|
||||
@get:Rule
|
||||
val flakyTestRule = SignalFlakyTestRule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
db = SignalDatabase.instance!!.signalWritableDatabase
|
||||
@@ -177,6 +182,48 @@ class SQLiteDatabaseTest {
|
||||
db.setTransactionSuccessful()
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun1.get())
|
||||
assertTrue(hasRun2.get())
|
||||
}
|
||||
|
||||
// @SignalFlakyTest
|
||||
// @Test
|
||||
fun runPostSuccessfulTransaction_runsAfterMainTransactionInNestedTransaction() {
|
||||
val hasRun1 = AtomicBoolean(false)
|
||||
val hasRun2 = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
hasRun1.set(true)
|
||||
}
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
assertTrue(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
hasRun2.set(true)
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.endTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun1.get())
|
||||
assertTrue(hasRun2.get())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -18,13 +20,10 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.groupChange
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.groupContext
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Optional
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName", "TestFunctionName")
|
||||
@@ -47,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)
|
||||
@@ -273,18 +272,41 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
assertThat("latest message should be deleted", sms.getMessageRecordOrNull(latestMessage.messageId), nullValue())
|
||||
}
|
||||
|
||||
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingTextMessage {
|
||||
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingMessage {
|
||||
wallClock++
|
||||
return IncomingTextMessage(sender, 1, wallClock, wallClock, wallClock, body, Optional.of(groupId), 0, true, null)
|
||||
return IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = sender,
|
||||
sentTimeMillis = wallClock,
|
||||
serverTimeMillis = wallClock,
|
||||
receivedTimeMillis = wallClock,
|
||||
body = body,
|
||||
groupId = groupId,
|
||||
isUnidentified = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingGroupUpdateMessage {
|
||||
return IncomingGroupUpdateMessage(smsMessage(sender, null), groupContext)
|
||||
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,
|
||||
update = updateDescription,
|
||||
isGroupAdd = false,
|
||||
serverGuid = null
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val aliceServiceId: ServiceId = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
private val bobServiceId: ServiceId = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
private val aliceServiceId: ACI = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
|
||||
private val bobServiceId: ACI = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
|
||||
|
||||
private val masterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
|
||||
private val groupId = GroupId.v2(masterKey)
|
||||
|
||||
@@ -12,19 +12,24 @@ import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StorySendTableTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(othersCount = 0, createGroup = false)
|
||||
|
||||
private val distributionId1 = DistributionId.from(UUID.randomUUID())
|
||||
private val distributionId2 = DistributionId.from(UUID.randomUUID())
|
||||
private val distributionId3 = DistributionId.from(UUID.randomUUID())
|
||||
@@ -460,7 +465,7 @@ class StorySendTableTest {
|
||||
|
||||
private fun makeRecipients(count: Int): List<RecipientId> {
|
||||
return (1..count).map {
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
|
||||
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
class ThreadTableTest_active {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule()
|
||||
|
||||
private lateinit var recipient: Recipient
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
|
||||
SignalDatabase.threads.getUnarchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
false,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(1, threads.count)
|
||||
|
||||
val record = ThreadTable.StaticReader(threads, InstrumentationRegistry.getInstrumentation().context).getNext()
|
||||
|
||||
assertNotNull(record)
|
||||
assertEquals(record!!.recipient.id, recipient.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenInactiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
|
||||
SignalDatabase.threads.getUnarchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
false,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(0, threads.count)
|
||||
}
|
||||
|
||||
val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
assertEquals(threadId2, threadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveArchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), true)
|
||||
|
||||
SignalDatabase.threads.getUnarchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
false,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(0, threads.count)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveArchivedThread_whenIGetArchivedConversationList_thenIExpectThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), true)
|
||||
|
||||
SignalDatabase.threads.getArchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(1, threads.count)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenInactiveArchivedThread_whenIGetArchivedConversationList_thenIExpectNoThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), true)
|
||||
|
||||
SignalDatabase.threads.getArchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(0, threads.count)
|
||||
}
|
||||
|
||||
val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
assertEquals(threadId2, threadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveArchivedThread_whenIDeactivateThread_thenIExpectNoMessages() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
|
||||
SignalDatabase.messages.getConversation(threadId).use {
|
||||
assertEquals(1, it.count)
|
||||
}
|
||||
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
|
||||
SignalDatabase.messages.getConversation(threadId).use {
|
||||
assertEquals(0, it.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import org.signal.core.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@@ -23,7 +23,7 @@ class ThreadTableTest_pinned {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.signal.core.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@@ -25,7 +25,7 @@ class ThreadTableTest_recents {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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
|
||||
@@ -13,9 +14,9 @@ import okio.ByteString
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.KbsEnclave
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
|
||||
import org.thoughtcrime.securesms.push.SignalServiceTrustStore
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipientCache
|
||||
@@ -23,32 +24,30 @@ 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.thoughtcrime.securesms.util.Base64
|
||||
import org.whispersystems.signalservice.api.KeyBackupService
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
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
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl
|
||||
import java.security.KeyStore
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url
|
||||
import java.util.Optional
|
||||
|
||||
/**
|
||||
* Dependency provider used for instrumentation tests (aka androidTests).
|
||||
*
|
||||
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess] and
|
||||
* [KeyBackupService].
|
||||
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess].
|
||||
*/
|
||||
class InstrumentationApplicationDependencyProvider(application: Application, 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 keyBackupService: KeyBackupService
|
||||
private val recipientCache: LiveRecipientCache
|
||||
private var signalServiceMessageSender: SignalServiceMessageSender? = null
|
||||
|
||||
init {
|
||||
runSync {
|
||||
@@ -59,33 +58,38 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
||||
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(
|
||||
arrayOf(SignalServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
mapOf(
|
||||
signalServiceUrls = arrayOf(SignalServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
signalCdnUrlMap = mapOf(
|
||||
0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT))
|
||||
),
|
||||
arrayOf(SignalKeyBackupServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
emptyList(),
|
||||
Optional.of(SignalServiceNetworkAccess.DNS),
|
||||
Optional.empty(),
|
||||
Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS)
|
||||
signalStorageUrls = arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
signalCdsiUrls = arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
signalSvr2Urls = arrayOf(SignalSvr2Url(baseUrl, serviceTrustStore, "localhost", ConnectionSpec.CLEARTEXT)),
|
||||
networkInterceptors = emptyList(),
|
||||
dns = Optional.of(SignalServiceNetworkAccess.DNS),
|
||||
signalProxy = Optional.empty(),
|
||||
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
|
||||
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS),
|
||||
backupServerPublicParams = Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS)
|
||||
)
|
||||
|
||||
serviceNetworkAccessMock = mock {
|
||||
@@ -94,8 +98,6 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
||||
on { uncensoredConfiguration } doReturn uncensoredConfiguration
|
||||
}
|
||||
|
||||
keyBackupService = mock()
|
||||
|
||||
recipientCache = LiveRecipientCache(application) { r -> r.run() }
|
||||
}
|
||||
|
||||
@@ -103,14 +105,21 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
||||
return serviceNetworkAccessMock
|
||||
}
|
||||
|
||||
override fun provideKeyBackupService(signalServiceAccountManager: SignalServiceAccountManager, keyStore: KeyStore, enclave: KbsEnclave): KeyBackupService {
|
||||
return keyBackupService
|
||||
}
|
||||
|
||||
override fun provideRecipientCache(): LiveRecipientCache {
|
||||
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"
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import android.net.Uri
|
||||
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.StreamUtil
|
||||
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.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import java.util.Optional
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AttachmentCompressionJobTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun testCompressionJobsWithDifferentTransformPropertiesCompleteSuccessfully() {
|
||||
val imageBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("images/sample_image.png").use {
|
||||
StreamUtil.readFully(it)
|
||||
}
|
||||
|
||||
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(AppDependencies.application)
|
||||
|
||||
val firstPreUpload = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
|
||||
val firstDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(firstPreUpload)
|
||||
|
||||
val firstCompressionJob: AttachmentCompressionJob = AttachmentCompressionJob.fromAttachment(firstDatabaseAttachment, false, -1)
|
||||
|
||||
var secondCompressionJob: AttachmentCompressionJob? = null
|
||||
var firstJobResult: Job.Result? = null
|
||||
var secondJobResult: Job.Result? = null
|
||||
|
||||
val secondJobLatch = CountDownLatch(1)
|
||||
val jobThread = Thread {
|
||||
firstCompressionJob.setContext(AppDependencies.application)
|
||||
firstJobResult = firstCompressionJob.run()
|
||||
|
||||
secondJobLatch.await()
|
||||
|
||||
secondCompressionJob!!.setContext(AppDependencies.application)
|
||||
secondJobResult = secondCompressionJob!!.run()
|
||||
}
|
||||
|
||||
jobThread.start()
|
||||
val secondPreUpload = createAttachment(1, blob, AttachmentTable.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH))
|
||||
val secondDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondPreUpload)
|
||||
secondCompressionJob = AttachmentCompressionJob.fromAttachment(secondDatabaseAttachment, false, -1)
|
||||
|
||||
secondJobLatch.countDown()
|
||||
|
||||
jobThread.join()
|
||||
|
||||
firstJobResult!!.isSuccess assertIs true
|
||||
secondJobResult!!.isSuccess assertIs true
|
||||
}
|
||||
|
||||
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties): UriAttachment {
|
||||
return UriAttachmentBuilder.build(
|
||||
id,
|
||||
uri = uri,
|
||||
contentType = MediaUtil.IMAGE_JPEG,
|
||||
transformProperties = transformProperties
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.EventTimer
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.JobDatabase.Companion.getInstance
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager
|
||||
import org.thoughtcrime.securesms.jobmanager.JobMigrator
|
||||
import org.thoughtcrime.securesms.jobmanager.JobTracker
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.random.Random
|
||||
|
||||
@Ignore("This is just for testing performance, not correctness, and they can therefore take a long time. Run them manually when you need to.")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class JobManagerPerformanceTests {
|
||||
|
||||
companion object {
|
||||
val TAG = Log.tag(JobManagerPerformanceTests::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformance_singleQueue() {
|
||||
runTest("singleQueue", 2000) { TestJob(queue = "queue") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformance_fourQueues() {
|
||||
runTest("fourQueues", 2000) { TestJob(queue = "queue-${Random.nextInt(1, 5)}") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPerformance_noQueues() {
|
||||
runTest("noQueues", 2000) { TestJob(queue = null) }
|
||||
}
|
||||
|
||||
private fun runTest(name: String, count: Int, jobCreator: () -> TestJob) {
|
||||
val context = AppDependencies.application
|
||||
val jobManager = testJobManager(context)
|
||||
|
||||
jobManager.beginJobLoop()
|
||||
|
||||
val eventTimer = EventTimer()
|
||||
|
||||
val latch = CountDownLatch(count)
|
||||
var seenStart = false
|
||||
jobManager.addListener({ it.factoryKey == TestJob.KEY }) { _, state ->
|
||||
if (!seenStart && state == JobTracker.JobState.RUNNING) {
|
||||
// Adding the jobs can take a while (and runs on a background thread), so we want to reset the timer the first time we see a job run so the first job
|
||||
// doesn't have a skewed time
|
||||
eventTimer.reset()
|
||||
seenStart = true
|
||||
}
|
||||
if (state.isComplete) {
|
||||
eventTimer.emit("job")
|
||||
latch.countDown()
|
||||
if (latch.count % 100 == 0L) {
|
||||
Log.d(TAG, "[$name] Finished ${count - latch.count}/$count jobs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i(TAG, "[$name] Adding jobs...")
|
||||
jobManager.addAll((1..count).map { jobCreator() })
|
||||
|
||||
Log.i(TAG, "[$name] Waiting for jobs to complete...")
|
||||
latch.await()
|
||||
Log.i(TAG, "[$name] Jobs complete!")
|
||||
Log.i(TAG, eventTimer.stop().summary)
|
||||
}
|
||||
|
||||
private fun testJobManager(context: Application): JobManager {
|
||||
val config = JobManager.Configuration.Builder()
|
||||
.setJobFactories(
|
||||
JobManagerFactories.getJobFactories(context) + mapOf(
|
||||
TestJob.KEY to TestJob.Factory()
|
||||
)
|
||||
)
|
||||
.setConstraintFactories(JobManagerFactories.getConstraintFactories(context))
|
||||
.setConstraintObservers(JobManagerFactories.getConstraintObservers(context))
|
||||
.setJobStorage(FastJobStorage(getInstance(context)))
|
||||
.setJobMigrator(JobMigrator(TextSecurePreferences.getJobManagerVersion(context), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(context)))
|
||||
.build()
|
||||
|
||||
return JobManager(context, config)
|
||||
}
|
||||
|
||||
private class TestJob(params: Parameters) : Job(params) {
|
||||
companion object {
|
||||
const val KEY = "test"
|
||||
}
|
||||
|
||||
constructor(queue: String?) : this(Parameters.Builder().setQueue(queue).build())
|
||||
|
||||
override fun serialize(): ByteArray? = null
|
||||
override fun getFactoryKey(): String = KEY
|
||||
override fun run(): Result = Result.success()
|
||||
override fun onFailure() = Unit
|
||||
|
||||
class Factory : Job.Factory<TestJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): TestJob {
|
||||
return TestJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
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.parsedRequestBody
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyState
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyStatus
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PreKeysSyncJobTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private val aciPreKeyMeta: PreKeyMetadataStore
|
||||
get() = SignalStore.account().aciPreKeys
|
||||
|
||||
private val pniPreKeyMeta: PreKeyMetadataStore
|
||||
get() = SignalStore.account().pniPreKeys
|
||||
|
||||
private lateinit var job: PreKeysSyncJob
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
job = PreKeysSyncJob()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create signed prekeys for both identities when both do not have registered prekeys according
|
||||
* to our local state.
|
||||
*/
|
||||
@Test
|
||||
fun runWithoutRegisteredKeysForBothIdentities() {
|
||||
// GIVEN
|
||||
aciPreKeyMeta.isSignedPreKeyRegistered = false
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered = false
|
||||
|
||||
lateinit var aciSignedPreKey: SignedPreKeyEntity
|
||||
lateinit var pniSignedPreKey: SignedPreKeyEntity
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v2/keys/signed?identity=aci") { r ->
|
||||
aciSignedPreKey = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Put("/v2/keys/signed?identity=pni") { r ->
|
||||
pniSignedPreKey = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
|
||||
aciPreKeyMeta.isSignedPreKeyRegistered assertIs true
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered assertIs true
|
||||
|
||||
val aciVerifySignatureResult = Curve.verifySignature(
|
||||
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.publicKey,
|
||||
aciSignedPreKey.publicKey.serialize(),
|
||||
aciSignedPreKey.signature
|
||||
)
|
||||
aciVerifySignatureResult assertIs true
|
||||
|
||||
val pniVerifySignatureResult = Curve.verifySignature(
|
||||
ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.publicKey,
|
||||
pniSignedPreKey.publicKey.serialize(),
|
||||
pniSignedPreKey.signature
|
||||
)
|
||||
pniVerifySignatureResult assertIs true
|
||||
}
|
||||
|
||||
/**
|
||||
* With 100 prekeys registered for each identity, do nothing.
|
||||
*/
|
||||
@Test
|
||||
fun runWithRegisteredKeysForBothIdentities() {
|
||||
// GIVEN
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(100)) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIs currentPniKeyId
|
||||
}
|
||||
|
||||
/**
|
||||
* With 100 prekeys registered for ACI, but no PNI prekeys registered according to local state,
|
||||
* do nothing for ACI but create PNI prekeys and update local state.
|
||||
*/
|
||||
@Test
|
||||
fun runWithRegisteredKeysForAciIdentityOnly() {
|
||||
// GIVEN
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered = false
|
||||
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
|
||||
Put("/v2/keys/signed?identity=pni") { MockResponse().success() }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered assertIs true
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
|
||||
}
|
||||
|
||||
/**
|
||||
* With <10 prekeys registered for each identity, upload new.
|
||||
*/
|
||||
@Test
|
||||
fun runWithLowNumberOfRegisteredKeysForBothIdentities() {
|
||||
// GIVEN
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
val currentNextAciPreKeyId = aciPreKeyMeta.nextOneTimePreKeyId
|
||||
val currentNextPniPreKeyId = pniPreKeyMeta.nextOneTimePreKeyId
|
||||
|
||||
lateinit var aciPreKeyStateRequest: PreKeyState
|
||||
lateinit var pniPreKeyStateRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(5)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(5)) },
|
||||
Put("/v2/keys/?identity=aci") { r ->
|
||||
aciPreKeyStateRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Put("/v2/keys/?identity=pni") { r ->
|
||||
pniPreKeyStateRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIsNot currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
|
||||
|
||||
aciPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextAciPreKeyId
|
||||
pniPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextPniPreKeyId
|
||||
|
||||
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.let { aciIdentityKey ->
|
||||
aciPreKeyStateRequest.identityKey assertIs aciIdentityKey
|
||||
|
||||
val verifySignatureResult = Curve.verifySignature(
|
||||
aciIdentityKey.publicKey,
|
||||
aciPreKeyStateRequest.signedPreKey.publicKey.serialize(),
|
||||
aciPreKeyStateRequest.signedPreKey.signature
|
||||
)
|
||||
verifySignatureResult assertIs true
|
||||
}
|
||||
|
||||
ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.let { pniIdentityKey ->
|
||||
pniPreKeyStateRequest.identityKey assertIs pniIdentityKey
|
||||
|
||||
val verifySignatureResult = Curve.verifySignature(
|
||||
pniIdentityKey.publicKey,
|
||||
pniPreKeyStateRequest.signedPreKey.publicKey.serialize(),
|
||||
pniPreKeyStateRequest.signedPreKey.signature
|
||||
)
|
||||
verifySignatureResult assertIs true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.libsignal.usernames.Username
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.Put
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.failure
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
||||
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
|
||||
import org.whispersystems.util.Base64UrlSafe
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RefreshOwnProfileJob__checkUsernameIsInSyncTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
SignalStore.phoneNumberPrivacy().clearUsernameOutOfSync()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoLocalUsername_whenICheckUsernameIsInSync_thenIExpectNoFailures() {
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenLocalUsernameDoesNotMatchServerUsername_whenICheckUsernameIsInSync_thenIExpectRetry() {
|
||||
// GIVEN
|
||||
var didReserve = false
|
||||
var didConfirm = false
|
||||
val username = "hello.32"
|
||||
val serverUsername = "hello.3232"
|
||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/accounts/whoami") { r ->
|
||||
MockResponse().success(
|
||||
WhoAmIResponse().apply {
|
||||
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(serverUsername))
|
||||
}
|
||||
)
|
||||
},
|
||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
||||
didReserve = true
|
||||
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
|
||||
},
|
||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
||||
didConfirm = true
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
|
||||
// THEN
|
||||
assertTrue(didReserve)
|
||||
assertTrue(didConfirm)
|
||||
assertFalse(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenLocalAndNoServer_whenICheckUsernameIsInSync_thenIExpectRetry() {
|
||||
// GIVEN
|
||||
var didReserve = false
|
||||
var didConfirm = false
|
||||
val username = "hello.32"
|
||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/accounts/whoami") { r ->
|
||||
MockResponse().success(WhoAmIResponse())
|
||||
},
|
||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
||||
didReserve = true
|
||||
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
|
||||
},
|
||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
||||
didConfirm = true
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
|
||||
// THEN
|
||||
assertTrue(didReserve)
|
||||
assertTrue(didConfirm)
|
||||
assertFalse(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenLocalAndServerMatch_whenICheckUsernameIsInSync_thenIExpectNoRetry() {
|
||||
// GIVEN
|
||||
var didReserve = false
|
||||
var didConfirm = false
|
||||
val username = "hello.32"
|
||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/accounts/whoami") { r ->
|
||||
MockResponse().success(
|
||||
WhoAmIResponse().apply {
|
||||
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))
|
||||
}
|
||||
)
|
||||
},
|
||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
||||
didReserve = true
|
||||
MockResponse().success(ReserveUsernameResponse(Base64UrlSafe.encodeBytesWithoutPadding(Username.hash(username))))
|
||||
},
|
||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
||||
didConfirm = true
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
|
||||
// THEN
|
||||
assertFalse(didReserve)
|
||||
assertFalse(didConfirm)
|
||||
assertFalse(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenMismatchAndReservationFails_whenICheckUsernameIsInSync_thenIExpectNoConfirm() {
|
||||
// GIVEN
|
||||
var didReserve = false
|
||||
var didConfirm = false
|
||||
val username = "hello.32"
|
||||
SignalDatabase.recipients.setUsername(harness.self.id, username)
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/accounts/whoami") { r ->
|
||||
MockResponse().success(
|
||||
WhoAmIResponse().apply {
|
||||
usernameHash = Base64UrlSafe.encodeBytesWithoutPadding(Username.hash("${username}23"))
|
||||
}
|
||||
)
|
||||
},
|
||||
Put("/v1/accounts/username_hash/reserve") { r ->
|
||||
didReserve = true
|
||||
MockResponse().failure(418)
|
||||
},
|
||||
Put("/v1/accounts/username_hash/confirm") { r ->
|
||||
didConfirm = true
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
|
||||
// THEN
|
||||
assertTrue(didReserve)
|
||||
assertFalse(didConfirm)
|
||||
assertTrue(SignalStore.phoneNumberPrivacy().isUsernameOutOfSync)
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.signal.core.util.Hex;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.signal.libsignal.svr2.PinHash;
|
||||
import org.whispersystems.signalservice.api.kbs.KbsData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -21,77 +22,76 @@ public final class PinHashing_hashPin_Test {
|
||||
@Test
|
||||
public void argon2_hashed_pin_password() throws IOException {
|
||||
String pin = "password";
|
||||
byte[] backupId = Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
|
||||
byte[] salt = Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
PinHash hashedPin = PinHashUtil.hashPin(pin, salt);
|
||||
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
|
||||
|
||||
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("3f33ce58eb25b40436592a30eae2a8fabab1899095f4e2fba6e2d0dc43b4a2d9cac5a3931748522393951e0e54dec769"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
|
||||
String localPinHash = PinHashing.localPinHash(pin);
|
||||
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
|
||||
String localPinHash = PinHashUtil.localPinHash(pin);
|
||||
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argon2_hashed_pin_another_password() throws IOException {
|
||||
String pin = "anotherpassword";
|
||||
byte[] backupId = Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f");
|
||||
byte[] salt = Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("88a787415a2ecd79da0d1016a82a27c5c695c9a19b88b0aa1d35683280aa9a67"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
PinHash hashedPin = PinHashUtil.hashPin(pin, salt);
|
||||
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
|
||||
|
||||
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("9d9b05402ea39c17ff1c9298c8a0e86784a352aa02a74943bf8bcf07ec0f4b574a5b786ad0182c8d308d9eb06538b8c9"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
|
||||
String localPinHash = PinHashing.localPinHash(pin);
|
||||
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
|
||||
String localPinHash = PinHashUtil.localPinHash(pin);
|
||||
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argon2_hashed_pin_password_with_spaces_diacritics_and_non_arabic_numerals() throws IOException {
|
||||
String pin = " Pass६örd ";
|
||||
byte[] backupId = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8");
|
||||
byte[] salt = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("9571f3fde1e58588ba49bcf82be1b301ca3859a6f59076f79a8f47181ef952bf"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
PinHash hashedPin = PinHashUtil.hashPin(pin, salt);
|
||||
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
|
||||
|
||||
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("ab645acdccc1652a48a34b2ac6926340ff35c03034013f68760f20013f028dd8"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("11c0ba1834db15e47c172f6c987c64bd4cfc69c6047dd67a022afeec0165a10943f204d5b8f37b3cb7bab21c6dfc39c8"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
|
||||
assertEquals("577939bccb2b6638c39222d5a97998a867c5e154e30b82cc120f2dd07a3de987", kbsData.getMasterKey().deriveRegistrationLock());
|
||||
|
||||
String localPinHash = PinHashing.localPinHash(pin);
|
||||
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
|
||||
String localPinHash = PinHashUtil.localPinHash(pin);
|
||||
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argon2_hashed_pin_password_with_just_non_arabic_numerals() throws IOException {
|
||||
String pin = " ६१८ ";
|
||||
byte[] backupId = Hex.fromStringCondensed("717dc111a98423a57196512606822fca646c653facd037c10728f14ba0be2ab3");
|
||||
byte[] salt = Hex.fromStringCondensed("717dc111a98423a57196512606822fca646c653facd037c10728f14ba0be2ab3");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("0432d735b32f66d0e3a70d4f9cc821a8529521a4937d26b987715d8eff4e4c54"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
PinHash hashedPin = PinHashUtil.hashPin(pin, salt);
|
||||
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
|
||||
|
||||
|
||||
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("d2fedabd0d4c17a371491c9722578843a26be3b4923e28d452ab2fc5491e794b"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("877ef871ef1fc668401c717ef21aa12e8523579fb1ff4474b76f28c2293537c80cc7569996c9e0229bea7f378e3a824e"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
|
||||
assertEquals("23a75cb1df1a87df45cc2ed167c2bdc85ab1220b847c88761b0005cac907fce5", kbsData.getMasterKey().deriveRegistrationLock());
|
||||
|
||||
String localPinHash = PinHashing.localPinHash(pin);
|
||||
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
|
||||
String localPinHash = PinHashUtil.localPinHash(pin);
|
||||
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,10 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableUtils
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage
|
||||
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.EditMessage
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -38,7 +39,6 @@ class EditMessageSyncProcessorTest {
|
||||
)
|
||||
|
||||
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
|
||||
AttachmentTable.UNIQUE_ID,
|
||||
AttachmentTable.TRANSFER_FILE
|
||||
)
|
||||
}
|
||||
@@ -46,13 +46,13 @@ class EditMessageSyncProcessorTest {
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var processorV2: MessageContentProcessorV2
|
||||
private lateinit var processorV2: MessageContentProcessor
|
||||
private lateinit var testResult: TestResults
|
||||
private var envelopeTimestamp: Long = 0
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
processorV2 = MessageContentProcessorV2(harness.context)
|
||||
processorV2 = MessageContentProcessor(harness.context)
|
||||
envelopeTimestamp = System.currentTimeMillis()
|
||||
testResult = TestResults()
|
||||
}
|
||||
@@ -67,16 +67,17 @@ class EditMessageSyncProcessorTest {
|
||||
|
||||
val content = MessageContentFuzzer.fuzzTextMessage()
|
||||
val metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, toRecipient.id)
|
||||
val syncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
||||
.setDestinationUuid(metadata.destinationServiceId.toString())
|
||||
.setTimestamp(originalTimestamp)
|
||||
.setExpirationStartTimestamp(originalTimestamp)
|
||||
.setMessage(content.dataMessage)
|
||||
)
|
||||
val syncContent = Content.Builder().syncMessage(
|
||||
SyncMessage.Builder().sent(
|
||||
SyncMessage.Sent.Builder()
|
||||
.destinationServiceId(metadata.destinationServiceId.toString())
|
||||
.timestamp(originalTimestamp)
|
||||
.expirationStartTimestamp(originalTimestamp)
|
||||
.message(content.dataMessage)
|
||||
.build()
|
||||
).build()
|
||||
).build()
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer)
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0, content.dataMessage?.expireTimerVersion ?: 1)
|
||||
val syncTextMessage = TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(originalTimestamp),
|
||||
content = syncContent,
|
||||
@@ -86,18 +87,20 @@ class EditMessageSyncProcessorTest {
|
||||
|
||||
val editTimestamp = originalTimestamp + 200
|
||||
val editedContent = MessageContentFuzzer.fuzzTextMessage()
|
||||
val editSyncContent = SignalServiceProtos.Content.newBuilder().setSyncMessage(
|
||||
SignalServiceProtos.SyncMessage.newBuilder().setSent(
|
||||
SignalServiceProtos.SyncMessage.Sent.newBuilder()
|
||||
.setDestinationUuid(metadata.destinationServiceId.toString())
|
||||
.setTimestamp(editTimestamp)
|
||||
.setExpirationStartTimestamp(editTimestamp)
|
||||
.setEditMessage(
|
||||
EditMessage.newBuilder()
|
||||
.setDataMessage(editedContent.dataMessage)
|
||||
.setTargetSentTimestamp(originalTimestamp)
|
||||
val editSyncContent = Content.Builder().syncMessage(
|
||||
SyncMessage.Builder().sent(
|
||||
SyncMessage.Sent.Builder()
|
||||
.destinationServiceId(metadata.destinationServiceId.toString())
|
||||
.timestamp(editTimestamp)
|
||||
.expirationStartTimestamp(editTimestamp)
|
||||
.editMessage(
|
||||
EditMessage.Builder()
|
||||
.dataMessage(editedContent.dataMessage)
|
||||
.targetSentTimestamp(originalTimestamp)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.build()
|
||||
).build()
|
||||
).build()
|
||||
|
||||
val syncEditMessage = TestMessage(
|
||||
@@ -109,38 +112,38 @@ class EditMessageSyncProcessorTest {
|
||||
|
||||
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
|
||||
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer / 1000)
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000, content.dataMessage?.expireTimerVersion ?: 1)
|
||||
val originalTextMessage = OutgoingMessage(
|
||||
threadRecipient = toRecipient,
|
||||
sentTimeMillis = originalTimestamp,
|
||||
body = content.dataMessage.body,
|
||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
||||
body = content.dataMessage?.body ?: "",
|
||||
expiresIn = content.dataMessage?.expireTimer?.seconds?.inWholeMilliseconds ?: 0,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = content.dataMessage.bodyRangesList.toBodyRangeList()
|
||||
bodyRanges = content.dataMessage?.bodyRanges.toBodyRangeList()
|
||||
)
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(toRecipient)
|
||||
val originalMessageId = SignalDatabase.messages.insertMessageOutbox(originalTextMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(originalMessageId, true)
|
||||
if (content.dataMessage.expireTimer > 0) {
|
||||
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
|
||||
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
|
||||
}
|
||||
|
||||
val editMessage = OutgoingMessage(
|
||||
threadRecipient = toRecipient,
|
||||
sentTimeMillis = editTimestamp,
|
||||
body = editedContent.dataMessage.body,
|
||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
||||
body = editedContent.dataMessage?.body ?: "",
|
||||
expiresIn = content.dataMessage?.expireTimer?.seconds?.inWholeMilliseconds ?: 0,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = editedContent.dataMessage.bodyRangesList.toBodyRangeList(),
|
||||
bodyRanges = editedContent.dataMessage?.bodyRanges.toBodyRangeList(),
|
||||
messageToEdit = originalMessageId
|
||||
)
|
||||
|
||||
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(editMessageId, true)
|
||||
|
||||
if (content.dataMessage.expireTimer > 0) {
|
||||
if ((content.dataMessage?.expireTimer ?: 0) > 0) {
|
||||
SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp)
|
||||
}
|
||||
testResult.collectLocal()
|
||||
@@ -167,7 +170,7 @@ class EditMessageSyncProcessorTest {
|
||||
|
||||
fun runSync(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.hasSyncMessage()) {
|
||||
if (content.syncMessage != null) {
|
||||
processorV2.process(
|
||||
envelope,
|
||||
content,
|
||||
@@ -238,7 +241,7 @@ class EditMessageSyncProcessorTest {
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||
data = MessageTableUtils.typeColumnToString(cursor.getLong(index))
|
||||
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
|
||||
column to data
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.junit.Rule
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.TestProtos
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
|
||||
abstract class MessageContentProcessorTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
protected fun MessageContentProcessor.doProcess(
|
||||
messageState: MessageState = MessageState.DECRYPTED_OK,
|
||||
content: SignalServiceContent,
|
||||
exceptionMetadata: ExceptionMetadata = ExceptionMetadata("sender", 1),
|
||||
timestamp: Long = 100L,
|
||||
smsMessageId: Long = -1L
|
||||
) {
|
||||
process(messageState, content, exceptionMetadata, timestamp, smsMessageId)
|
||||
}
|
||||
|
||||
protected fun createNormalContentTestSubject(): MessageContentProcessor {
|
||||
val context = ApplicationProvider.getApplicationContext<Application>()
|
||||
|
||||
return MessageContentProcessor.create(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a valid ServiceContentProto with a data message which can be built via
|
||||
* `injectDataMessage`. This function is intended to be built on-top of for more
|
||||
* specific scenario in subclasses.
|
||||
*
|
||||
* Example can be seen in __handleStoryMessageTest
|
||||
*/
|
||||
protected fun createServiceContentWithDataMessage(
|
||||
messageSender: Recipient = Recipient.resolved(harness.others.first()),
|
||||
injectDataMessage: SignalServiceProtos.DataMessage.Builder.() -> Unit
|
||||
): SignalServiceContentProto {
|
||||
return TestProtos.build {
|
||||
serviceContent(
|
||||
localAddress = address(uuid = harness.self.requireServiceId().uuid()).build(),
|
||||
metadata = metadata(
|
||||
address = address(uuid = messageSender.requireServiceId().uuid()).build()
|
||||
).build()
|
||||
).apply {
|
||||
content = content().apply {
|
||||
dataMessage = dataMessage().apply {
|
||||
injectDataMessage()
|
||||
}.build()
|
||||
}.build()
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.database.Cursor
|
||||
import android.util.Base64
|
||||
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.core.util.ThreadUtil
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Entry
|
||||
import org.thoughtcrime.securesms.testing.InMemoryLogger
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableUtils
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceMetadata
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceAddressProtobufSerializer
|
||||
import org.whispersystems.signalservice.internal.serialize.SignalServiceMetadataProtobufSerializer
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
import java.util.Optional
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageContentProcessorTestV2 {
|
||||
|
||||
companion object {
|
||||
private val TAGS = listOf(MessageContentProcessor.TAG, MessageContentProcessorV2.TAG, AttachmentTable.TAG)
|
||||
|
||||
private val GENERALIZE_TAG = mapOf(
|
||||
MessageContentProcessor.TAG to "MCP",
|
||||
MessageContentProcessorV2.TAG to "MCP",
|
||||
AttachmentTable.TAG to AttachmentTable.TAG
|
||||
)
|
||||
|
||||
private val IGNORE_MESSAGE_COLUMNS = listOf(
|
||||
MessageTable.DATE_RECEIVED,
|
||||
MessageTable.NOTIFIED_TIMESTAMP,
|
||||
MessageTable.REACTIONS_LAST_SEEN,
|
||||
MessageTable.NOTIFIED
|
||||
)
|
||||
|
||||
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
|
||||
AttachmentTable.UNIQUE_ID,
|
||||
AttachmentTable.TRANSFER_FILE
|
||||
)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var processorV1: MessageContentProcessor
|
||||
private lateinit var processorV2: MessageContentProcessorV2
|
||||
private lateinit var testResult: TestResults
|
||||
private var envelopeTimestamp: Long = 0
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
processorV1 = MessageContentProcessor(harness.context)
|
||||
processorV2 = MessageContentProcessorV2(harness.context)
|
||||
envelopeTimestamp = System.currentTimeMillis()
|
||||
testResult = TestResults()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textMessage() {
|
||||
var start = envelopeTimestamp
|
||||
|
||||
val messages: List<TestMessage> = (0 until 100).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
testResult.runV2(messages)
|
||||
testResult.runV1(messages)
|
||||
|
||||
testResult.assert()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mediaMessage() {
|
||||
var start = envelopeTimestamp
|
||||
|
||||
val textMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
val firstBatchMediaMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzMediaMessageWithBody(textMessages),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
val secondBatchNoContentMediaMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzMediaMessageNoContent(textMessages + firstBatchMediaMessages),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
val thirdBatchNoTextMediaMessagesMessages: List<TestMessage> = (0 until 10).map {
|
||||
start += 200
|
||||
TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(start),
|
||||
content = MessageContentFuzzer.fuzzMediaMessageNoText(textMessages + firstBatchMediaMessages),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.others[0], harness.self.id),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(start)
|
||||
)
|
||||
}
|
||||
|
||||
testResult.runV2(textMessages + firstBatchMediaMessages + secondBatchNoContentMediaMessages + thirdBatchNoTextMediaMessagesMessages)
|
||||
testResult.runV1(textMessages + firstBatchMediaMessages + secondBatchNoContentMediaMessages + thirdBatchNoTextMediaMessagesMessages)
|
||||
|
||||
testResult.assert()
|
||||
}
|
||||
|
||||
private inner class TestResults {
|
||||
|
||||
private lateinit var v1Logs: List<Entry>
|
||||
private lateinit var v1Messages: List<List<Pair<String, String?>>>
|
||||
private lateinit var v1Attachments: List<List<Pair<String, String?>>>
|
||||
private lateinit var v2Logs: List<Entry>
|
||||
private lateinit var v2Messages: List<List<Pair<String, String?>>>
|
||||
private lateinit var v2Attachments: List<List<Pair<String, String?>>>
|
||||
|
||||
fun runV1(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.hasDataMessage()) {
|
||||
processorV1.process(
|
||||
MessageContentProcessor.MessageState.DECRYPTED_OK,
|
||||
toSignalServiceContent(envelope, content, metadata, serverDeliveredTimestamp),
|
||||
null,
|
||||
envelope.timestamp,
|
||||
-1
|
||||
)
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
v1Logs = harness.inMemoryLogger.logs()
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
v1Messages = dumpMessages()
|
||||
v1Attachments = dumpAttachments()
|
||||
}
|
||||
|
||||
fun runV2(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.hasDataMessage()) {
|
||||
processorV2.process(
|
||||
envelope,
|
||||
content,
|
||||
metadata,
|
||||
serverDeliveredTimestamp,
|
||||
false
|
||||
)
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
}
|
||||
|
||||
v2Logs = harness.inMemoryLogger.logs()
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
v2Messages = dumpMessages()
|
||||
v2Attachments = dumpAttachments()
|
||||
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
SignalDatabase.rawDatabase.withinTransaction { db ->
|
||||
SignalDatabase.threads.deleteAllConversations()
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${MessageTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${ThreadTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${AttachmentTable.TABLE_NAME}'")
|
||||
}
|
||||
}
|
||||
|
||||
fun assert() {
|
||||
v2Logs.zip(v1Logs)
|
||||
.forEach { (v2, v1) ->
|
||||
GENERALIZE_TAG[v2.tag]!!.assertIs(GENERALIZE_TAG[v1.tag]!!)
|
||||
|
||||
if (v2.tag != AttachmentTable.TAG) {
|
||||
if (v2.message?.startsWith("[") == true && v1.message?.startsWith("[") == false) {
|
||||
v2.message!!.substring(v2.message!!.indexOf(']') + 2).assertIs(v1.message)
|
||||
} else {
|
||||
v2.message.assertIs(v1.message)
|
||||
}
|
||||
} else {
|
||||
if (v2.message?.startsWith("Inserted attachment at ID: AttachmentId::") == true) {
|
||||
v2.message!!
|
||||
.substring(0, v2.message!!.indexOf(','))
|
||||
.assertIs(
|
||||
v1.message!!
|
||||
.substring(0, v1.message!!.indexOf(','))
|
||||
)
|
||||
} else {
|
||||
v2.message.assertIs(v1.message)
|
||||
}
|
||||
}
|
||||
v2.throwable.assertIs(v1.throwable)
|
||||
}
|
||||
|
||||
v2Messages.zip(v1Messages)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
|
||||
v2Attachments.zip(v1Attachments)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun InMemoryLogger.logs(): List<Entry> {
|
||||
return entries()
|
||||
.filter { TAGS.contains(it.tag) }
|
||||
}
|
||||
|
||||
private fun dumpMessages(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(MessageTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_MESSAGE_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpAttachments(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(AttachmentTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_ATTACHMENT_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpTable(table: 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 = MessageTableUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
|
||||
column to data
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toSignalServiceContent(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long): SignalServiceContent {
|
||||
val localAddress = SignalServiceAddress(metadata.destinationServiceId, Optional.ofNullable(SignalStore.account().e164))
|
||||
val signalServiceMetadata = SignalServiceMetadata(
|
||||
SignalServiceAddress(metadata.sourceServiceId, Optional.ofNullable(metadata.sourceE164)),
|
||||
metadata.sourceDeviceId,
|
||||
envelope.timestamp,
|
||||
envelope.serverTimestamp,
|
||||
serverDeliveredTimestamp,
|
||||
metadata.sealedSender,
|
||||
envelope.serverGuid,
|
||||
Optional.ofNullable(metadata.groupId),
|
||||
metadata.destinationServiceId.toString()
|
||||
)
|
||||
|
||||
val contentProto = SignalServiceContentProto.newBuilder()
|
||||
.setLocalAddress(SignalServiceAddressProtobufSerializer.toProtobuf(localAddress))
|
||||
.setMetadata(SignalServiceMetadataProtobufSerializer.toProtobuf(signalServiceMetadata))
|
||||
.setContent(content)
|
||||
.build()
|
||||
|
||||
return SignalServiceContent.createFromProto(contentProto)!!
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MmsHelper
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.TestProtos
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
import kotlin.random.Random
|
||||
|
||||
@Suppress("ClassName")
|
||||
class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorTest() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenContentWithADirectStoryReplyWhenIProcessThenIInsertAReplyInTheCorrectThread() {
|
||||
val sender = Recipient.resolved(harness.others.first())
|
||||
val senderThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(sender)
|
||||
val myStory = Recipient.resolved(SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!!)
|
||||
val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
|
||||
val expectedSentTime = 200L
|
||||
val storyMessageId = MmsHelper.insert(
|
||||
sentTimeMillis = expectedSentTime,
|
||||
recipient = myStory,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = myStoryThread
|
||||
)
|
||||
|
||||
SignalDatabase.storySends.insert(
|
||||
messageId = storyMessageId,
|
||||
recipientIds = listOf(sender.id),
|
||||
sentTimestamp = expectedSentTime,
|
||||
allowsReplies = true,
|
||||
distributionId = DistributionId.MY_STORY
|
||||
)
|
||||
|
||||
val expectedBody = "Hello!"
|
||||
|
||||
val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext(
|
||||
messageSender = sender,
|
||||
storyAuthor = harness.self,
|
||||
storySentTimestamp = expectedSentTime
|
||||
) {
|
||||
body = expectedBody
|
||||
}
|
||||
|
||||
runTestWithContent(contentProto = storyContent)
|
||||
|
||||
val replyId = SignalDatabase.messages.getConversation(senderThreadId, 0, 1).use {
|
||||
it.moveToFirst()
|
||||
it.requireLong(MessageTable.ID)
|
||||
}
|
||||
|
||||
val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord
|
||||
assertEquals(ParentStoryId.DirectReply(storyMessageId).serialize(), replyRecord.parentStoryId!!.serialize())
|
||||
assertEquals(expectedBody, replyRecord.body)
|
||||
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenContentWithAGroupStoryReplyWhenIProcessThenIInsertAReplyToTheCorrectStory() {
|
||||
val sender = Recipient.resolved(harness.others[0])
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(
|
||||
listOf(
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(harness.self.requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build(),
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(sender.requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
)
|
||||
)
|
||||
.setRevision(0)
|
||||
.build()
|
||||
|
||||
val group = SignalDatabase.groups.create(
|
||||
groupMasterKey,
|
||||
decryptedGroupState
|
||||
)
|
||||
|
||||
val groupRecipient = Recipient.externalGroupExact(group)
|
||||
val threadForGroup = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
|
||||
|
||||
val insertResult = MmsHelper.insert(
|
||||
message = IncomingMediaMessage(
|
||||
from = sender.id,
|
||||
sentTimeMillis = 100L,
|
||||
serverTimeMillis = 101L,
|
||||
receivedTimeMillis = 102L,
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
),
|
||||
threadId = threadForGroup
|
||||
)
|
||||
|
||||
val expectedBody = "Hello, World!"
|
||||
val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext(
|
||||
messageSender = sender,
|
||||
storyAuthor = sender,
|
||||
storySentTimestamp = 100L
|
||||
) {
|
||||
groupV2 = TestProtos.build { groupContextV2(masterKeyBytes = groupMasterKey.serialize()).build() }
|
||||
body = expectedBody
|
||||
}
|
||||
|
||||
runTestWithContent(storyContent)
|
||||
|
||||
val replyId = SignalDatabase.messages.getStoryReplies(insertResult.get().messageId).use { cursor ->
|
||||
assertEquals(1, cursor.count)
|
||||
cursor.moveToFirst()
|
||||
cursor.requireLong(MessageTable.ID)
|
||||
}
|
||||
|
||||
val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord
|
||||
assertEquals(ParentStoryId.GroupReply(insertResult.get().messageId).serialize(), replyRecord.parentStoryId?.serialize())
|
||||
assertEquals(threadForGroup, replyRecord.threadId)
|
||||
assertEquals(expectedBody, replyRecord.body)
|
||||
|
||||
SignalDatabase.messages.deleteGroupStoryReplies(insertResult.get().messageId)
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ServiceContent proto with a StoryContext, and then
|
||||
* uses `injectDataMessage` to fill in the data message object.
|
||||
*/
|
||||
private fun createServiceContentWithStoryContext(
|
||||
messageSender: Recipient,
|
||||
storyAuthor: Recipient,
|
||||
storySentTimestamp: Long,
|
||||
injectDataMessage: DataMessage.Builder.() -> Unit
|
||||
): SignalServiceContentProto {
|
||||
return createServiceContentWithDataMessage(messageSender) {
|
||||
storyContext = TestProtos.build {
|
||||
storyContext(
|
||||
sentTimestamp = storySentTimestamp,
|
||||
authorUuid = storyAuthor.requireServiceId().toString()
|
||||
).build()
|
||||
}
|
||||
injectDataMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestWithContent(contentProto: SignalServiceContentProto) {
|
||||
val content = SignalServiceContent.createFromProto(contentProto)
|
||||
val testSubject = createNormalContentTestSubject()
|
||||
testSubject.doProcess(content = content!!)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
|
||||
@Suppress("ClassName")
|
||||
class MessageContentProcessor__handleTextMessageTest : MessageContentProcessorTest() {
|
||||
@Test
|
||||
fun givenContentWithATextMessageWhenIProcessThenIInsertTheTextMessage() {
|
||||
val testSubject: MessageContentProcessor = createNormalContentTestSubject()
|
||||
val expectedBody = "Hello, World!"
|
||||
val contentProto: SignalServiceContentProto = createServiceContentWithDataMessage {
|
||||
body = expectedBody
|
||||
}
|
||||
|
||||
val content = SignalServiceContent.createFromProto(contentProto)
|
||||
|
||||
// WHEN
|
||||
testSubject.doProcess(content = content!!)
|
||||
|
||||
// THEN
|
||||
val record = SignalDatabase.messages.getMessageRecord(1)
|
||||
val threadSize = SignalDatabase.messages.getMessageCountForThread(record.threadId)
|
||||
assertEquals(1, threadSize)
|
||||
|
||||
assertTrue(record.isSecure)
|
||||
assertEquals(expectedBody, record.body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageContentProcessor__recipientStatusTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var processor: MessageContentProcessor
|
||||
private var envelopeTimestamp: Long = 0
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
processor = MessageContentProcessor(harness.context)
|
||||
envelopeTimestamp = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
/**
|
||||
* Process sync group sent text transcript with partial send and then process second sync with recipient update
|
||||
* flag set to true with the rest of the send completed.
|
||||
*/
|
||||
@Test
|
||||
fun syncGroupSentTextMessageWithRecipientUpdateFollowup() {
|
||||
val (groupId, masterKey, groupRecipientId) = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), harness.others[0].asMember(), harness.others[1].asMember())
|
||||
val groupContextV2 = GroupContextV2.Builder().revision(0).masterKey(masterKey.serialize().toByteString()).build()
|
||||
|
||||
val initialTextMessage = DataMessage.Builder().buildWith {
|
||||
body = MessageContentFuzzer.string()
|
||||
groupV2 = groupContextV2
|
||||
timestamp = envelopeTimestamp
|
||||
}
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
||||
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(groupRecipientId)!!
|
||||
val firstSyncMessages = MessageTableTestUtils.getMessages(threadId)
|
||||
val firstMessageId = firstSyncMessages[0].id
|
||||
val firstReceiptInfo = SignalDatabase.groupReceipts.getGroupReceiptInfo(firstMessageId)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
||||
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0], harness.others[1]), recipientUpdate = true),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
val secondSyncMessages = MessageTableTestUtils.getMessages(threadId)
|
||||
val secondReceiptInfo = SignalDatabase.groupReceipts.getGroupReceiptInfo(firstMessageId)
|
||||
|
||||
firstSyncMessages.size assertIs 1
|
||||
firstSyncMessages[0].body assertIs initialTextMessage.body
|
||||
firstReceiptInfo.first { it.recipientId == harness.others[0] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED
|
||||
firstReceiptInfo.first { it.recipientId == harness.others[1] }.status assertIs GroupReceiptTable.STATUS_UNKNOWN
|
||||
|
||||
secondSyncMessages.size assertIs 1
|
||||
secondSyncMessages[0].body assertIs initialTextMessage.body
|
||||
secondReceiptInfo.first { it.recipientId == harness.others[0] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED
|
||||
secondReceiptInfo.first { it.recipientId == harness.others[1] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED
|
||||
}
|
||||
}
|
||||
@@ -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,9 +6,9 @@ import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
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
|
||||
@@ -16,7 +16,7 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.AliceClient
|
||||
@@ -25,9 +25,9 @@ import org.thoughtcrime.securesms.testing.Entry
|
||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.awaitFor
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketMessage
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketProtos.WebSocketRequestMessage
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketMessage
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
@@ -37,7 +37,7 @@ import android.util.Log as AndroidLog
|
||||
/**
|
||||
* Sends N messages from Bob to Alice to track performance of Alice's processing of messages.
|
||||
*/
|
||||
// @Ignore("Ignore test in normal testing as it's a performance test with no assertions")
|
||||
@Ignore("Ignore test in normal testing as it's a performance test with no assertions")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageProcessingPerformanceTest {
|
||||
|
||||
@@ -55,17 +55,17 @@ class MessageProcessingPerformanceTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic(UnidentifiedAccessUtil::class)
|
||||
every { UnidentifiedAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
|
||||
mockkStatic(SealedSenderAccessUtil::class)
|
||||
every { SealedSenderAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
|
||||
|
||||
mockkObject(MessageContentProcessorV2)
|
||||
every { MessageContentProcessorV2.create(harness.application) } returns TimingMessageContentProcessorV2(harness.application)
|
||||
mockkObject(MessageContentProcessor)
|
||||
every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application)
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
unmockkStatic(UnidentifiedAccessUtil::class)
|
||||
unmockkStatic(MessageContentProcessorV2::class)
|
||||
unmockkStatic(SealedSenderAccessUtil::class)
|
||||
unmockkStatic(MessageContentProcessor::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,7 +92,7 @@ class MessageProcessingPerformanceTest {
|
||||
val messageCount = 100
|
||||
val envelopes = generateInboundEnvelopes(bobClient, messageCount)
|
||||
val firstTimestamp = envelopes.first().timestamp
|
||||
val lastTimestamp = envelopes.last().timestamp
|
||||
val lastTimestamp = envelopes.last().timestamp ?: 0
|
||||
|
||||
// Inject the envelopes into the websocket
|
||||
Thread {
|
||||
@@ -106,7 +106,7 @@ class MessageProcessingPerformanceTest {
|
||||
// Wait until they've all been fully decrypted + processed
|
||||
harness
|
||||
.inMemoryLogger
|
||||
.getLockForUntil(TimingMessageContentProcessorV2.endTagPredicate(lastTimestamp))
|
||||
.getLockForUntil(TimingMessageContentProcessor.endTagPredicate(lastTimestamp))
|
||||
.awaitFor(1.minutes)
|
||||
|
||||
harness.inMemoryLogger.flush()
|
||||
@@ -125,7 +125,7 @@ class MessageProcessingPerformanceTest {
|
||||
|
||||
// Calculate MessageContentProcessor
|
||||
|
||||
val takeLast: List<Entry> = entries.filter { it.tag == TimingMessageContentProcessorV2.TAG }.drop(2)
|
||||
val takeLast: List<Entry> = entries.filter { it.tag == TimingMessageContentProcessor.TAG }.drop(2)
|
||||
val iterator = takeLast.iterator()
|
||||
var processCount = 0L
|
||||
var processDuration = 0L
|
||||
@@ -141,7 +141,7 @@ class MessageProcessingPerformanceTest {
|
||||
// Calculate messages per second from "retrieving" first message post session initialization to processing last message
|
||||
|
||||
val start = entries.first { it.message == "Retrieved envelope! $firstTimestamp" }
|
||||
val end = entries.first { it.message == TimingMessageContentProcessorV2.endTag(lastTimestamp) }
|
||||
val end = entries.first { it.message == TimingMessageContentProcessor.endTag(lastTimestamp) }
|
||||
|
||||
val duration = (end.timestamp - start.timestamp).toFloat() / 1000f
|
||||
val messagePerSecond = messageCount.toFloat() / duration
|
||||
@@ -156,7 +156,7 @@ class MessageProcessingPerformanceTest {
|
||||
|
||||
val aliceProcessFirstMessageLatch = harness
|
||||
.inMemoryLogger
|
||||
.getLockForUntil(TimingMessageContentProcessorV2.endTagPredicate(firstPreKeyMessageTimestamp))
|
||||
.getLockForUntil(TimingMessageContentProcessor.endTagPredicate(firstPreKeyMessageTimestamp))
|
||||
|
||||
Thread { aliceClient.process(encryptedEnvelope, System.currentTimeMillis()) }.start()
|
||||
aliceProcessFirstMessageLatch.awaitFor(15.seconds)
|
||||
@@ -178,32 +178,19 @@ class MessageProcessingPerformanceTest {
|
||||
}
|
||||
|
||||
private fun webSocketTombstone(): ByteString {
|
||||
return WebSocketMessage
|
||||
.newBuilder()
|
||||
.setRequest(
|
||||
WebSocketRequestMessage.newBuilder()
|
||||
.setVerb("PUT")
|
||||
.setPath("/api/v1/queue/empty")
|
||||
)
|
||||
.build()
|
||||
.toByteArray()
|
||||
.toByteString()
|
||||
return WebSocketMessage(request = WebSocketRequestMessage(verb = "PUT", path = "/api/v1/queue/empty")).encodeByteString()
|
||||
}
|
||||
|
||||
private fun Envelope.toWebSocketPayload(): ByteString {
|
||||
return WebSocketMessage
|
||||
.newBuilder()
|
||||
.setType(WebSocketMessage.Type.REQUEST)
|
||||
.setRequest(
|
||||
WebSocketRequestMessage.newBuilder()
|
||||
.setVerb("PUT")
|
||||
.setPath("/api/v1/message")
|
||||
.setId(Random(System.currentTimeMillis()).nextLong())
|
||||
.addHeaders("X-Signal-Timestamp: ${this.timestamp}")
|
||||
.setBody(this.toByteString())
|
||||
return WebSocketMessage(
|
||||
type = WebSocketMessage.Type.REQUEST,
|
||||
request = WebSocketRequestMessage(
|
||||
verb = "PUT",
|
||||
path = "/api/v1/message",
|
||||
id = Random(System.currentTimeMillis()).nextLong(),
|
||||
headers = listOf("X-Signal-Timestamp: ${this.timestamp}"),
|
||||
body = this.encodeByteString()
|
||||
)
|
||||
.build()
|
||||
.toByteArray()
|
||||
.toByteString()
|
||||
).encodeByteString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.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.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SyncMessageProcessorTest_readSyncs {
|
||||
|
||||
@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 handleSynchronizeReadMessage() {
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadMessageMissingTimestamp() {
|
||||
messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEdits() {
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
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(messageHelper.alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEditsInGroup() {
|
||||
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
|
||||
|
||||
messageHelper.syncReadMessage(messageHelper.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 message2Timestamp = messageHelper.incomingMedia(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(messageHelper.bob to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,703 @@
|
||||
/*
|
||||
* 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.Ignore
|
||||
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
|
||||
}
|
||||
|
||||
@Ignore("counts are consistent for some reason")
|
||||
@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)
|
||||
|
||||
IdentityUtil.markIdentityUpdate(harness.context, alice.id)
|
||||
|
||||
// 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.calls.insertAcceptedGroupCall(4, messageHelper.group.recipientId, CallTable.Direction.INCOMING, System.currentTimeMillis())
|
||||
SignalDatabase.calls.insertDeclinedGroupCall(5, messageHelper.group.recipientId, System.currentTimeMillis())
|
||||
|
||||
// Detected changes
|
||||
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
|
||||
SignalDatabase.messages.insertLearnedProfileNameChangeMessage(alice, null, "username.42")
|
||||
SignalDatabase.messages.insertNumberChangeMessages(alice.id)
|
||||
SignalDatabase.messages.insertSmsExportMessage(alice.id, SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!)
|
||||
SignalDatabase.messages.insertSessionSwitchoverEvent(alice.id, aliceThreadId, SessionSwitchoverEvent())
|
||||
|
||||
// Sent failed
|
||||
SignalDatabase.messages.markAsSending(messageHelper.outgoingText().messageId)
|
||||
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
|
||||
messageHelper.outgoingText().let {
|
||||
SignalDatabase.messages.markAsSending(it.messageId)
|
||||
SignalDatabase.messages.markAsRateLimited(it.messageId)
|
||||
}
|
||||
|
||||
// Group change
|
||||
messageHelper.outgoingGroupChange()
|
||||
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
|
||||
data class TestMessage(
|
||||
val envelope: SignalServiceProtos.Envelope,
|
||||
val content: SignalServiceProtos.Content,
|
||||
val envelope: Envelope,
|
||||
val content: Content,
|
||||
val metadata: EnvelopeMetadata,
|
||||
val serverDeliveredTimestamp: Long
|
||||
)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.testing.LogPredicate
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
|
||||
class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(context) {
|
||||
companion object {
|
||||
val TAG = Log.tag(TimingMessageContentProcessor::class.java)
|
||||
|
||||
fun endTagPredicate(timestamp: Long): LogPredicate = { entry ->
|
||||
entry.tag == TAG && entry.message == endTag(timestamp)
|
||||
}
|
||||
|
||||
private fun startTag(timestamp: Long) = "$timestamp start"
|
||||
fun endTag(timestamp: Long) = "$timestamp end"
|
||||
}
|
||||
|
||||
override fun process(envelope: Envelope, content: Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean, localMetric: SignalLocalMetrics.MessageReceive?) {
|
||||
Log.d(TAG, startTag(envelope.timestamp!!))
|
||||
super.process(envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent, localMetric)
|
||||
Log.d(TAG, endTag(envelope.timestamp!!))
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.testing.LogPredicate
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
|
||||
class TimingMessageContentProcessorV2(context: Context) : MessageContentProcessorV2(context) {
|
||||
companion object {
|
||||
val TAG = Log.tag(TimingMessageContentProcessorV2::class.java)
|
||||
|
||||
fun endTagPredicate(timestamp: Long): LogPredicate = { entry ->
|
||||
entry.tag == TAG && entry.message == endTag(timestamp)
|
||||
}
|
||||
|
||||
private fun startTag(timestamp: Long) = "$timestamp start"
|
||||
fun endTag(timestamp: Long) = "$timestamp end"
|
||||
}
|
||||
|
||||
override fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean) {
|
||||
Log.d(TAG, startTag(envelope.timestamp))
|
||||
super.process(envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent)
|
||||
Log.d(TAG, endTag(envelope.timestamp))
|
||||
}
|
||||
}
|
||||
@@ -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.inAppPayments.setSubscriberCurrency(Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
SignalStore.inAppPayments.setSubscriber("USD", subscriberId)
|
||||
SignalStore.inAppPayments.setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
|
||||
SignalStore.inAppPayments.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
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIsNotNull
|
||||
import org.thoughtcrime.securesms.testing.assertIsNull
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.api.util.Usernames
|
||||
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -56,27 +57,10 @@ class UsernameEditFragmentTest {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUsernameCreationInRegistration() {
|
||||
val scenario = createScenario(true)
|
||||
|
||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
|
||||
noViewFoundException.assertIsNull()
|
||||
val toolbar = view as Toolbar
|
||||
|
||||
toolbar.navigationIcon.assertIsNull()
|
||||
}
|
||||
|
||||
onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed()))
|
||||
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
|
||||
}
|
||||
|
||||
@Ignore("Flakey espresso test.")
|
||||
@Test
|
||||
fun testUsernameCreationOutsideOfRegistration() {
|
||||
val scenario = createScenario()
|
||||
val scenario = createScenario(UsernameEditMode.NORMAL)
|
||||
|
||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
@@ -96,7 +80,7 @@ class UsernameEditFragmentTest {
|
||||
fun testNicknameUpdateHappyPath() {
|
||||
val nickname = "Spiderman"
|
||||
val discriminator = "4578"
|
||||
val username = "$nickname${UsernameState.DELIMITER}$discriminator"
|
||||
val username = "$nickname${Usernames.DELIMITER}$discriminator"
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v1/accounts/username/reserved") {
|
||||
@@ -107,7 +91,7 @@ class UsernameEditFragmentTest {
|
||||
}
|
||||
)
|
||||
|
||||
val scenario = createScenario(isInRegistration = true)
|
||||
val scenario = createScenario(UsernameEditMode.NORMAL)
|
||||
scenario.moveToState(Lifecycle.State.RESUMED)
|
||||
|
||||
onView(withId(R.id.username_text)).perform(typeText(nickname))
|
||||
@@ -131,8 +115,8 @@ class UsernameEditFragmentTest {
|
||||
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
|
||||
}
|
||||
|
||||
private fun createScenario(isInRegistration: Boolean = false): FragmentScenario<UsernameEditFragment> {
|
||||
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle()
|
||||
private fun createScenario(mode: UsernameEditMode = UsernameEditMode.NORMAL): FragmentScenario<UsernameEditFragment> {
|
||||
val fragmentArgs = UsernameEditFragmentArgs.Builder().setMode(mode).build().toBundle()
|
||||
return launchFragmentInContainer(
|
||||
fragmentArgs = fragmentArgs,
|
||||
themeResId = R.style.Signal_DayNight_NoActionBar
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,14 @@ import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
@@ -26,38 +24,71 @@ class ContactRecordProcessorTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
SignalStore.account().setE164(E164_SELF)
|
||||
SignalStore.account().setAci(ACI_SELF)
|
||||
SignalStore.account().setPni(PNI_SELF)
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
SignalStore.account.setE164(E164_SELF)
|
||||
SignalStore.account.setAci(ACI_SELF)
|
||||
SignalStore.account.setPni(PNI_SELF)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun process_splitContact_normalSplit() {
|
||||
fun process_splitContact_normalSplit_twoRecords() {
|
||||
// GIVEN
|
||||
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||
setStorageId(originalId, STORAGE_ID_A)
|
||||
|
||||
val remote1 = buildRecord(STORAGE_ID_B) {
|
||||
setServiceId(ACI_A.toString())
|
||||
setUnregisteredAtTimestamp(100)
|
||||
}
|
||||
val remote1 = buildRecord(
|
||||
STORAGE_ID_B,
|
||||
ContactRecord(
|
||||
aci = ACI_A.toString(),
|
||||
unregisteredAtTimestamp = 100
|
||||
)
|
||||
)
|
||||
|
||||
val remote2 = buildRecord(STORAGE_ID_C) {
|
||||
setServiceId(PNI_A.toString())
|
||||
setServicePni(PNI_A.toString())
|
||||
setServiceE164(E164_A)
|
||||
}
|
||||
val remote2 = buildRecord(
|
||||
STORAGE_ID_C,
|
||||
ContactRecord(
|
||||
pni = PNI_A.toString(),
|
||||
e164 = E164_A
|
||||
)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val subject = ContactRecordProcessor()
|
||||
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
|
||||
|
||||
// THEN
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByServiceId(PNI_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
assertEquals(originalId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
assertNotEquals(byAci, byE164)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun process_splitContact_normalSplit_oneRecord() {
|
||||
// GIVEN
|
||||
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||
setStorageId(originalId, STORAGE_ID_A)
|
||||
|
||||
val remote = buildRecord(
|
||||
STORAGE_ID_B,
|
||||
ContactRecord(
|
||||
aci = ACI_A.toString(),
|
||||
unregisteredAtTimestamp = 100
|
||||
)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val subject = ContactRecordProcessor()
|
||||
subject.process(listOf(remote), StorageSyncHelper.KEY_GENERATOR)
|
||||
|
||||
// THEN
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
assertEquals(originalId, byAci)
|
||||
assertEquals(byE164, byPni)
|
||||
@@ -70,23 +101,29 @@ class ContactRecordProcessorTest {
|
||||
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
|
||||
setStorageId(originalId, STORAGE_ID_A)
|
||||
|
||||
val remote1 = buildRecord(STORAGE_ID_B) {
|
||||
setServiceId(ACI_A.toString())
|
||||
setUnregisteredAtTimestamp(0)
|
||||
}
|
||||
val remote1 = buildRecord(
|
||||
STORAGE_ID_B,
|
||||
ContactRecord(
|
||||
aci = ACI_A.toString(),
|
||||
unregisteredAtTimestamp = 0
|
||||
)
|
||||
)
|
||||
|
||||
val remote2 = buildRecord(STORAGE_ID_C) {
|
||||
setServiceId(PNI_A.toString())
|
||||
setServicePni(PNI_A.toString())
|
||||
setServiceE164(E164_A)
|
||||
}
|
||||
val remote2 = buildRecord(
|
||||
STORAGE_ID_C,
|
||||
ContactRecord(
|
||||
aci = PNI_A.toString(),
|
||||
pni = PNI_A.toString(),
|
||||
e164 = E164_A
|
||||
)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val subject = ContactRecordProcessor()
|
||||
subject.process(listOf(remote1, remote2), StorageSyncHelper.KEY_GENERATOR)
|
||||
|
||||
// THEN
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByServiceId(ACI_A).get()
|
||||
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
|
||||
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
|
||||
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
|
||||
|
||||
@@ -95,14 +132,14 @@ class ContactRecordProcessorTest {
|
||||
assertEquals(byAci, byE164)
|
||||
}
|
||||
|
||||
private fun buildRecord(id: StorageId, applyParams: ContactRecord.Builder.() -> ContactRecord.Builder): SignalContactRecord {
|
||||
return SignalContactRecord(id, ContactRecord.getDefaultInstance().toBuilder().applyParams().build())
|
||||
private fun buildRecord(id: StorageId, record: ContactRecord): SignalContactRecord {
|
||||
return SignalContactRecord(id, record)
|
||||
}
|
||||
|
||||
private fun setStorageId(recipientId: RecipientId, storageId: StorageId) {
|
||||
SignalDatabase.rawDatabase
|
||||
.update(RecipientTable.TABLE_NAME)
|
||||
.values(RecipientTable.STORAGE_SERVICE_ID to Base64.encodeBytes(storageId.raw))
|
||||
.values(RecipientTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(storageId.raw))
|
||||
.where("${RecipientTable.ID} = ?", recipientId)
|
||||
.run()
|
||||
}
|
||||
|
||||
@@ -3,15 +3,14 @@ package org.thoughtcrime.securesms.testing
|
||||
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
|
||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
|
||||
/**
|
||||
* Welcome to Alice's Client.
|
||||
@@ -27,20 +26,20 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
|
||||
|
||||
private val aliceSenderCertificate = FakeClientHelpers.createCertificateFor(
|
||||
trustRoot = trustRoot,
|
||||
uuid = serviceId.uuid(),
|
||||
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 { ApplicationDependencies.getJobManager().add(it) }
|
||||
?.forEach { it.enqueue() }
|
||||
|
||||
bufferedStore.flushToDisk()
|
||||
val end = System.currentTimeMillis()
|
||||
@@ -48,9 +47,9 @@ 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),
|
||||
FakeClientHelpers.getSealedSenderAccess(ProfileKey(destination.profileKey), aliceSenderCertificate),
|
||||
1,
|
||||
FakeClientHelpers.encryptedTextMessage(now),
|
||||
false
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord
|
||||
import org.signal.libsignal.protocol.state.IdentityKeyStore
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.PreKeyBundle
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SessionRecord
|
||||
@@ -16,7 +17,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
|
||||
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyTable
|
||||
@@ -24,14 +25,13 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
|
||||
import org.whispersystems.signalservice.api.SignalSessionLock
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
|
||||
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
|
||||
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import java.util.Optional
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
@@ -48,7 +48,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
private val serviceAddress = SignalServiceAddress(serviceId, e164)
|
||||
private val registrationId = KeyHelper.generateRegistrationId(false)
|
||||
private val aciStore = BobSignalServiceAccountDataStore(registrationId, identityKeyPair)
|
||||
private val senderCertificate = FakeClientHelpers.createCertificateFor(trustRoot, serviceId.uuid(), e164, 1, identityKeyPair.publicKey.publicKey, 31337)
|
||||
private val senderCertificate = FakeClientHelpers.createCertificateFor(trustRoot, serviceId.rawUuid, e164, 1, identityKeyPair.publicKey.publicKey, 31337)
|
||||
private val sessionLock = object : SignalSessionLock {
|
||||
private val lock = ReentrantLock()
|
||||
|
||||
@@ -59,7 +59,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
}
|
||||
|
||||
/** Inspired by SignalServiceMessageSender#getEncryptedMessage */
|
||||
fun encrypt(now: Long): SignalServiceProtos.Envelope {
|
||||
fun encrypt(now: Long): Envelope {
|
||||
val envelopeContent = FakeClientHelpers.encryptedTextMessage(now)
|
||||
|
||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, null)
|
||||
@@ -70,16 +70,16 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
}
|
||||
|
||||
return cipher.encrypt(getAliceProtocolAddress(), getAliceUnidentifiedAccess(), envelopeContent)
|
||||
.toEnvelope(envelopeContent.content.get().dataMessage.timestamp, getAliceServiceId())
|
||||
.toEnvelope(envelopeContent.content.get().dataMessage!!.timestamp!!, getAliceServiceId())
|
||||
}
|
||||
|
||||
fun decrypt(envelope: SignalServiceProtos.Envelope, serverDeliveredTimestamp: Long) {
|
||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator())
|
||||
fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) {
|
||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, SealedSenderAccessUtil.getCertificateValidator())
|
||||
cipher.decrypt(envelope, serverDeliveredTimestamp)
|
||||
}
|
||||
|
||||
private fun getAliceServiceId(): ServiceId {
|
||||
return SignalStore.account().requireAci()
|
||||
return SignalStore.account.requireAci()
|
||||
}
|
||||
|
||||
private fun getAlicePreKeyBundle(): PreKeyBundle {
|
||||
@@ -102,7 +102,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,
|
||||
@@ -114,19 +114,19 @@ 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 {
|
||||
return ProfileKeyUtil.getSelfProfileKey()
|
||||
}
|
||||
|
||||
private fun getAliceUnidentifiedAccess(): Optional<UnidentifiedAccess> {
|
||||
return FakeClientHelpers.getTargetUnidentifiedAccess(profileKey, getAliceProfileKey(), senderCertificate)
|
||||
private fun getAliceUnidentifiedAccess(): SealedSenderAccess? {
|
||||
return FakeClientHelpers.getSealedSenderAccess(getAliceProfileKey(), senderCertificate)
|
||||
}
|
||||
|
||||
private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore {
|
||||
@@ -138,11 +138,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()
|
||||
@@ -155,13 +156,25 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
override fun storeSignedPreKey(signedPreKeyId: Int, record: SignedPreKeyRecord?) = throw UnsupportedOperationException()
|
||||
override fun containsSignedPreKey(signedPreKeyId: Int): Boolean = throw UnsupportedOperationException()
|
||||
override fun removeSignedPreKey(signedPreKeyId: Int) = throw UnsupportedOperationException()
|
||||
override fun loadKyberPreKey(kyberPreKeyId: Int): KyberPreKeyRecord = throw UnsupportedOperationException()
|
||||
override fun loadKyberPreKeys(): MutableList<KyberPreKeyRecord> = throw UnsupportedOperationException()
|
||||
override fun storeKyberPreKey(kyberPreKeyId: Int, record: KyberPreKeyRecord?) = throw UnsupportedOperationException()
|
||||
override fun containsKyberPreKey(kyberPreKeyId: Int): Boolean = throw UnsupportedOperationException()
|
||||
override fun markKyberPreKeyUsed(kyberPreKeyId: Int) = throw UnsupportedOperationException()
|
||||
override fun deleteAllStaleOneTimeEcPreKeys(threshold: Long, minCount: Int) = throw UnsupportedOperationException()
|
||||
override fun markAllOneTimeEcPreKeysStaleIfNecessary(staleTime: Long) = throw UnsupportedOperationException()
|
||||
override fun storeSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?, record: SenderKeyRecord?) = throw UnsupportedOperationException()
|
||||
override fun loadSenderKey(sender: SignalProtocolAddress?, distributionId: UUID?): SenderKeyRecord = throw UnsupportedOperationException()
|
||||
override fun archiveSession(address: SignalProtocolAddress?) = throw UnsupportedOperationException()
|
||||
override fun getAllAddressesWithActiveSessions(addressNames: MutableList<String>?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
|
||||
override fun getAllAddressesWithActiveSessions(addressNames: MutableList<String>?): MutableMap<SignalProtocolAddress, SessionRecord> = throw UnsupportedOperationException()
|
||||
override fun getSenderKeySharedWith(distributionId: DistributionId?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
|
||||
override fun markSenderKeySharedWith(distributionId: DistributionId?, addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||
override fun clearSenderKeySharedWith(addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||
override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) = throw UnsupportedOperationException()
|
||||
override fun removeKyberPreKey(kyberPreKeyId: Int) = throw UnsupportedOperationException()
|
||||
override fun markAllOneTimeKyberPreKeysStaleIfNecessary(staleTime: Long) = throw UnsupportedOperationException()
|
||||
override fun deleteAllStaleOneTimeKyberPreKeys(threshold: Long, minCount: Int) = throw UnsupportedOperationException()
|
||||
override fun loadLastResortKyberPreKeys(): List<KyberPreKeyRecord> = throw UnsupportedOperationException()
|
||||
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.libsignal.internal.Native
|
||||
import org.signal.libsignal.internal.NativeHandleGuard
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator
|
||||
@@ -9,16 +11,16 @@ import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
|
||||
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.util.Base64
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@@ -44,17 +46,16 @@ object FakeClientHelpers {
|
||||
}
|
||||
}
|
||||
|
||||
fun getTargetUnidentifiedAccess(myProfileKey: ProfileKey, theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): Optional<UnidentifiedAccess> {
|
||||
val selfUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(myProfileKey)
|
||||
val themUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
|
||||
fun getSealedSenderAccess(theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): SealedSenderAccess? {
|
||||
val themUnidentifiedAccessKey = UnidentifiedAccess(UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey), senderCertificate.serialized, false)
|
||||
|
||||
return UnidentifiedAccessPair(UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate.serialized), UnidentifiedAccess(themUnidentifiedAccessKey, senderCertificate.serialized)).targetUnidentifiedAccess
|
||||
return SealedSenderAccess.forIndividual(themUnidentifiedAccessKey)
|
||||
}
|
||||
|
||||
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {
|
||||
val content = SignalServiceProtos.Content.newBuilder().apply {
|
||||
setDataMessage(
|
||||
SignalServiceProtos.DataMessage.newBuilder().apply {
|
||||
val content = Content.Builder().apply {
|
||||
dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
body = message
|
||||
timestamp = now
|
||||
}
|
||||
@@ -64,16 +65,16 @@ object FakeClientHelpers {
|
||||
}
|
||||
|
||||
fun OutgoingPushMessage.toEnvelope(timestamp: Long, destination: ServiceId): Envelope {
|
||||
return Envelope.newBuilder()
|
||||
.setType(Envelope.Type.valueOf(this.type))
|
||||
.setSourceDevice(1)
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 1)
|
||||
.setDestinationUuid(destination.toString())
|
||||
.setServerGuid(UUID.randomUUID().toString())
|
||||
.setContent(Base64.decode(this.content).toProtoByteString())
|
||||
.setUrgent(true)
|
||||
.setStory(false)
|
||||
return Envelope.Builder()
|
||||
.type(Envelope.Type.fromValue(this.type))
|
||||
.sourceDevice(1)
|
||||
.timestamp(timestamp)
|
||||
.serverTimestamp(timestamp + 1)
|
||||
.destinationServiceId(destination.toString())
|
||||
.serverGuid(UUID.randomUUID().toString())
|
||||
.content(Base64.decode(this.content).toByteString())
|
||||
.urgent(true)
|
||||
.story(false)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Helper methods for creating groups for message processing tests et al.
|
||||
*/
|
||||
object GroupTestingUtils {
|
||||
fun member(aci: ACI, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
|
||||
return DecryptedMember.Builder()
|
||||
.aciBytes(aci.toByteString())
|
||||
.joinedAtRevision(revision)
|
||||
.role(role)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun insertGroup(revision: Int = 0, vararg members: DecryptedMember): TestGroupInfo {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
val decryptedGroupState = DecryptedGroup.Builder()
|
||||
.members(members.toList())
|
||||
.revision(revision)
|
||||
.title(MessageContentFuzzer.string())
|
||||
.build()
|
||||
|
||||
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState, null)!!
|
||||
val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId)
|
||||
SignalDatabase.recipients.setProfileSharing(groupRecipientId, true)
|
||||
|
||||
return TestGroupInfo(groupId, groupMasterKey, groupRecipientId)
|
||||
}
|
||||
|
||||
fun RecipientId.asMember(): DecryptedMember {
|
||||
return Recipient.resolved(this).asMember()
|
||||
}
|
||||
|
||||
fun Recipient.asMember(): DecryptedMember {
|
||||
return member(aci = requireAci())
|
||||
}
|
||||
|
||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) {
|
||||
val groupV2Context: GroupContextV2
|
||||
get() = GroupContextV2(masterKey = masterKey.serialize().toByteString(), revision = 0)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user