Compare commits
1157 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
4
.github/workflows/android.yml
vendored
@@ -18,6 +18,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
@@ -30,7 +32,7 @@ jobs:
|
||||
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() }}
|
||||
|
||||
6
.github/workflows/diffuse.yml
vendored
@@ -15,6 +15,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: set up JDK 17
|
||||
@@ -37,7 +38,7 @@ jobs:
|
||||
|
||||
- name: Build with Gradle
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
run: ./gradlew assemblePlayProdRelease --parallel
|
||||
run: ./gradlew assemblePlayProdRelease
|
||||
|
||||
- name: Copy base apk
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
@@ -45,10 +46,11 @@ jobs:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
clean: 'false'
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assemblePlayProdRelease --parallel
|
||||
run: ./gradlew assemblePlayProdRelease
|
||||
|
||||
- name: Copy PR apk
|
||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk
|
||||
|
||||
3
.gitignore
vendored
@@ -3,6 +3,7 @@ captures/
|
||||
project.properties
|
||||
keystore.debug.properties
|
||||
keystore.staging.properties
|
||||
nightly-url.txt
|
||||
.project
|
||||
.settings
|
||||
bin/
|
||||
@@ -28,4 +29,4 @@ jni/libspeex/.deps/
|
||||
pkcs11.password
|
||||
dev.keystore
|
||||
maps.key
|
||||
local/
|
||||
local/
|
||||
|
||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "libwebp"]
|
||||
path = libwebp
|
||||
url = https://github.com/webmproject/libwebp.git
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Signal Android
|
||||
# Signal Android
|
||||
|
||||
Signal is a simple, powerful, and secure messenger.
|
||||
|
||||
@@ -54,7 +54,7 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013-2023 Signal
|
||||
Copyright 2013-2024 Signal Messenger, LLC
|
||||
|
||||
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
|
||||
2
apntool/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*.db
|
||||
*.db.gz
|
||||
@@ -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
|
||||
718
app/build.gradle
@@ -1,718 +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 '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'
|
||||
}
|
||||
|
||||
protoPath {
|
||||
srcDir "${project.rootDir}/libsignal/service/src/main/protowire"
|
||||
}
|
||||
}
|
||||
|
||||
ktlint {
|
||||
version = "0.49.1"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1325
|
||||
def canonicalVersionName = "6.32.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 = signalKotlinJvmTarget
|
||||
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.4.4'
|
||||
}
|
||||
|
||||
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_CDN3_URL", "\"https://cdn3.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\""
|
||||
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_CDN3_IPS", cdn3_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_CDSI_IPS", cdsi_ips
|
||||
buildConfigField "String[]", "SIGNAL_SVR2_IPS", svr2_ips
|
||||
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
|
||||
buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\""
|
||||
buildConfigField "String", "SVR2_MRENCLAVE", "\"6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094\""
|
||||
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", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\""
|
||||
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'
|
||||
}
|
||||
resourceConfigurations += []
|
||||
|
||||
|
||||
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_CDN3_URL", "\"https://cdn3-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\""
|
||||
buildConfigField "String", "SVR2_MRENCLAVE", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\""
|
||||
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
|
||||
"\"ee1d0d972b7ea903615670de43ab1b6e7a825e811c70a29bb5fe0f819e0975fa\", " +
|
||||
"\"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", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\""
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
android.buildTypes.each {
|
||||
if (it.name != 'release') {
|
||||
sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/debug/java"
|
||||
} else {
|
||||
sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/release/java"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.compose.rxjava3
|
||||
implementation libs.androidx.compose.runtime.livedata
|
||||
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.media3
|
||||
|
||||
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.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.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
|
||||
implementation libs.accompanist.permissions
|
||||
|
||||
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) { version { strictly "1.70" } } // Used by roboelectric
|
||||
testImplementation (testLibs.bouncycastle.bcpkix.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
|
||||
testImplementation testLibs.conscrypt.openjdk.uber // Used by robolectric
|
||||
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"
|
||||
}
|
||||
722
app/build.gradle.kts
Normal file
@@ -0,0 +1,722 @@
|
||||
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 = 1403
|
||||
val canonicalVersionName = "7.2.2"
|
||||
|
||||
val postFixSize = 100
|
||||
val abiPostFix: Map<String, Int> = mapOf(
|
||||
"universal" to 0,
|
||||
"armeabi-v7a" to 1,
|
||||
"arm64-v8a" to 2,
|
||||
"x86" to 3,
|
||||
"x86_64" to 4
|
||||
)
|
||||
|
||||
val 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 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("0.49.1")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.thoughtcrime.securesms"
|
||||
|
||||
buildToolsVersion = signalBuildToolsVersion
|
||||
compileSdkVersion = signalCompileSdkVersion
|
||||
|
||||
flavorDimensions += listOf("distribution", "environment")
|
||||
useLibrary("org.apache.http.legacy")
|
||||
testBuildType = "instrumentation"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
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", "libsignal_jni.dylib", "signal_jni.dll")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.4.4"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
versionCode = canonicalVersionCode * postFixSize
|
||||
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_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\"")
|
||||
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("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_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"")
|
||||
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
|
||||
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
|
||||
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
|
||||
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("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")) {
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
|
||||
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")
|
||||
|
||||
val abiName: String = output.getFilter("ABI") ?: "universal"
|
||||
val postFix: Int = abiPostFix[abiName]!!
|
||||
|
||||
if (postFix >= postFixSize) {
|
||||
throw AssertionError("postFix is too large")
|
||||
}
|
||||
|
||||
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
beforeVariants { variant ->
|
||||
variant.enable = variant.name in selectableVariants
|
||||
}
|
||||
}
|
||||
|
||||
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.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.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.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.jackson.module.kotlin)
|
||||
implementation(libs.rxjava3.rxandroid)
|
||||
implementation(libs.rxjava3.rxkotlin)
|
||||
implementation(libs.rxdogtag)
|
||||
|
||||
"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)
|
||||
|
||||
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() + "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*(**);
|
||||
}
|
||||
|
||||
@@ -26,15 +26,27 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
|
||||
}
|
||||
|
||||
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,675 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import androidx.core.content.contentValuesOf
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireBlob
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.toInt
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey
|
||||
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.database.EmojiSearchTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.subscription.Subscriber
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
typealias DatabaseData = Map<String, List<Map<String, Any?>>>
|
||||
|
||||
class BackupTest {
|
||||
companion object {
|
||||
val SELF_ACI = ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
|
||||
val SELF_PNI = PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
|
||||
const val SELF_E164 = "+10000000000"
|
||||
val SELF_PROFILE_KEY = ProfileKey(Random.nextBytes(32))
|
||||
|
||||
val ALICE_ACI = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
|
||||
val ALICE_PNI = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
|
||||
val ALICE_E164 = "+12222222222"
|
||||
|
||||
/** Columns that we don't need to check equality of */
|
||||
private val IGNORED_COLUMNS: Map<String, Set<String>> = mapOf(
|
||||
RecipientTable.TABLE_NAME to setOf(RecipientTable.STORAGE_SERVICE_ID),
|
||||
MessageTable.TABLE_NAME to setOf(MessageTable.FROM_DEVICE_ID)
|
||||
)
|
||||
|
||||
/** Tables we don't need to check equality of */
|
||||
private val IGNORED_TABLES: Set<String> = setOf(
|
||||
EmojiSearchTable.TABLE_NAME,
|
||||
"sqlite_sequence",
|
||||
"message_fts_data",
|
||||
"message_fts_idx",
|
||||
"message_fts_docsize"
|
||||
)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
SignalStore.account().setE164(SELF_E164)
|
||||
SignalStore.account().setAci(SELF_ACI)
|
||||
SignalStore.account().setPni(SELF_PNI)
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyDatabase() {
|
||||
backupTest { }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noteToSelf() {
|
||||
backupTest {
|
||||
individualChat(aci = SELF_ACI, givenName = "Note to Self") {
|
||||
standardMessage(outgoing = true, body = "A")
|
||||
standardMessage(outgoing = true, body = "B")
|
||||
standardMessage(outgoing = true, body = "C")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun individualChat() {
|
||||
backupTest {
|
||||
individualChat(aci = ALICE_ACI, givenName = "Alice") {
|
||||
val m1 = standardMessage(outgoing = true, body = "Outgoing 1")
|
||||
val m2 = standardMessage(outgoing = false, body = "Incoming 1", read = true)
|
||||
standardMessage(outgoing = true, body = "Outgoing 2", quotes = m2)
|
||||
standardMessage(outgoing = false, body = "Incoming 2", quotes = m1, quoteTargetMissing = true, read = false)
|
||||
standardMessage(outgoing = true, body = "Outgoing 3, with mention", randomMention = true)
|
||||
standardMessage(outgoing = false, body = "Incoming 3, with style", read = false, randomStyling = true)
|
||||
remoteDeletedMessage(outgoing = true)
|
||||
remoteDeletedMessage(outgoing = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun individualRecipients() {
|
||||
backupTest {
|
||||
// Comprehensive example
|
||||
individualRecipient(
|
||||
aci = ALICE_ACI,
|
||||
pni = ALICE_PNI,
|
||||
e164 = ALICE_E164,
|
||||
givenName = "Alice",
|
||||
familyName = "Smith",
|
||||
username = "alice.99",
|
||||
hidden = false,
|
||||
registeredState = RecipientTable.RegisteredState.REGISTERED,
|
||||
profileKey = ProfileKey(Random.nextBytes(32)),
|
||||
profileSharing = true,
|
||||
hideStory = false
|
||||
)
|
||||
|
||||
// Trying to get coverage of all the various values
|
||||
individualRecipient(aci = ACI.from(UUID.randomUUID()), registeredState = RecipientTable.RegisteredState.NOT_REGISTERED)
|
||||
individualRecipient(aci = ACI.from(UUID.randomUUID()), registeredState = RecipientTable.RegisteredState.UNKNOWN)
|
||||
individualRecipient(pni = PNI.from(UUID.randomUUID()))
|
||||
individualRecipient(e164 = "+15551234567")
|
||||
individualRecipient(aci = ACI.from(UUID.randomUUID()), givenName = "Bob")
|
||||
individualRecipient(aci = ACI.from(UUID.randomUUID()), familyName = "Smith")
|
||||
individualRecipient(aci = ACI.from(UUID.randomUUID()), profileSharing = false)
|
||||
individualRecipient(aci = ACI.from(UUID.randomUUID()), hideStory = true)
|
||||
individualRecipient(aci = ACI.from(UUID.randomUUID()), hidden = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun individualCallLogs() {
|
||||
backupTest {
|
||||
val aliceId = individualRecipient(
|
||||
aci = ALICE_ACI,
|
||||
pni = ALICE_PNI,
|
||||
e164 = ALICE_E164,
|
||||
givenName = "Alice",
|
||||
familyName = "Smith",
|
||||
username = "alice.99",
|
||||
hidden = false,
|
||||
registeredState = RecipientTable.RegisteredState.REGISTERED,
|
||||
profileKey = ProfileKey(Random.nextBytes(32)),
|
||||
profileSharing = true,
|
||||
hideStory = false
|
||||
)
|
||||
insertOneToOneCallVariations(1, 1, aliceId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun insertOneToOneCallVariations(callId: Long, timestamp: Long, id: RecipientId): Long {
|
||||
val directions = arrayOf(CallTable.Direction.INCOMING, CallTable.Direction.OUTGOING)
|
||||
val callTypes = arrayOf(CallTable.Type.AUDIO_CALL, CallTable.Type.VIDEO_CALL)
|
||||
val events = arrayOf(
|
||||
CallTable.Event.MISSED,
|
||||
CallTable.Event.OUTGOING_RING,
|
||||
CallTable.Event.ONGOING,
|
||||
CallTable.Event.ACCEPTED,
|
||||
CallTable.Event.NOT_ACCEPTED
|
||||
)
|
||||
var callTimestamp: Long = timestamp
|
||||
var currentCallId = callId
|
||||
for (direction in directions) {
|
||||
for (event in events) {
|
||||
for (type in callTypes) {
|
||||
insertOneToOneCall(callId = currentCallId, callTimestamp, id, type, direction, event)
|
||||
callTimestamp++
|
||||
currentCallId++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentCallId
|
||||
}
|
||||
|
||||
private fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: CallTable.Type, direction: CallTable.Direction, event: CallTable.Event) {
|
||||
val messageType: Long = CallTable.Call.getMessageType(type, direction, event)
|
||||
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
val recipient = Recipient.resolved(peer)
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
val outgoing = direction == CallTable.Direction.OUTGOING
|
||||
|
||||
val messageValues = contentValuesOf(
|
||||
MessageTable.FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else peer.serialize(),
|
||||
MessageTable.FROM_DEVICE_ID to 1,
|
||||
MessageTable.TO_RECIPIENT_ID to if (outgoing) peer.serialize() else Recipient.self().id.serialize(),
|
||||
MessageTable.DATE_RECEIVED to timestamp,
|
||||
MessageTable.DATE_SENT to timestamp,
|
||||
MessageTable.READ to 1,
|
||||
MessageTable.TYPE to messageType,
|
||||
MessageTable.THREAD_ID to threadId
|
||||
)
|
||||
|
||||
val messageId = SignalDatabase.rawDatabase.insert(MessageTable.TABLE_NAME, null, messageValues)
|
||||
|
||||
val values = contentValuesOf(
|
||||
CallTable.CALL_ID to callId,
|
||||
CallTable.MESSAGE_ID to messageId,
|
||||
CallTable.PEER to peer.serialize(),
|
||||
CallTable.TYPE to CallTable.Type.serialize(type),
|
||||
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
|
||||
CallTable.EVENT to CallTable.Event.serialize(event),
|
||||
CallTable.TIMESTAMP to timestamp
|
||||
)
|
||||
|
||||
SignalDatabase.rawDatabase.insert(CallTable.TABLE_NAME, null, values)
|
||||
|
||||
SignalDatabase.threads.update(threadId, true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun accountData() {
|
||||
val context = ApplicationDependencies.getApplication()
|
||||
|
||||
backupTest(validateKeyValue = true) {
|
||||
val self = Recipient.self()
|
||||
|
||||
// TODO note-to-self archived
|
||||
// TODO note-to-self unread
|
||||
|
||||
SignalStore.account().setAci(SELF_ACI)
|
||||
SignalStore.account().setPni(SELF_PNI)
|
||||
SignalStore.account().setE164(SELF_E164)
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
|
||||
SignalDatabase.recipients.setProfileKey(self.id, ProfileKey(Random.nextBytes(32)))
|
||||
SignalDatabase.recipients.setProfileName(self.id, ProfileName.fromParts("Peter", "Parker"))
|
||||
SignalDatabase.recipients.setProfileAvatar(self.id, "https://example.com/")
|
||||
|
||||
SignalStore.donationsValues().markUserManuallyCancelled()
|
||||
SignalStore.donationsValues().setSubscriber(Subscriber(SubscriberId.generate(), "USD"))
|
||||
SignalStore.donationsValues().setDisplayBadgesOnProfile(false)
|
||||
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
|
||||
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
|
||||
|
||||
SignalStore.settings().isLinkPreviewsEnabled = false
|
||||
SignalStore.settings().isPreferSystemContactPhotos = true
|
||||
SignalStore.settings().universalExpireTimer = 42
|
||||
SignalStore.settings().setKeepMutedChatsArchived(true)
|
||||
|
||||
SignalStore.storyValues().viewedReceiptsEnabled = false
|
||||
SignalStore.storyValues().userHasViewedOnboardingStory = true
|
||||
SignalStore.storyValues().isFeatureDisabled = false
|
||||
SignalStore.storyValues().userHasBeenNotifiedAboutStories = true
|
||||
SignalStore.storyValues().userHasSeenGroupStoryEducationSheet = true
|
||||
|
||||
SignalStore.emojiValues().reactions = listOf("a", "b", "c")
|
||||
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, false)
|
||||
TextSecurePreferences.setReadReceiptsEnabled(context, false)
|
||||
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, true)
|
||||
}
|
||||
|
||||
// Have to check TextSecurePreferences ourselves, since they're not in a database
|
||||
TextSecurePreferences.isTypingIndicatorsEnabled(context) assertIs false
|
||||
TextSecurePreferences.isReadReceiptsEnabled(context) assertIs false
|
||||
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context) assertIs true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the database, then executes your setup code, then compares snapshots of the database
|
||||
* before an after an import to ensure that no data was lost/changed.
|
||||
*
|
||||
* @param validateKeyValue If true, this will also validate the KeyValueDatabase. You only want to do this if you
|
||||
* intend on setting most of the values. Otherwise stuff tends to not match since values are lazily written.
|
||||
*/
|
||||
private fun backupTest(validateKeyValue: Boolean = false, content: () -> Unit) {
|
||||
// Under normal circumstances, My Story ends up being the first recipient in the table, and is added automatically.
|
||||
// This screws with the tests by offsetting all the recipientIds in the initial state.
|
||||
// Easiest way to get around this is to make the DB a true clean slate by clearing everything.
|
||||
// (We only really need to clear Recipient/dlists, but doing everything to be consistent.)
|
||||
SignalDatabase.distributionLists.clearAllDataForBackupRestore()
|
||||
SignalDatabase.recipients.clearAllDataForBackupRestore()
|
||||
SignalDatabase.messages.clearAllDataForBackupRestore()
|
||||
SignalDatabase.threads.clearAllDataForBackupRestore()
|
||||
|
||||
// Again, for comparison purposes, because we always import self first, we want to ensure it's the first item
|
||||
// in the table when we export.
|
||||
individualRecipient(
|
||||
aci = SELF_ACI,
|
||||
pni = SELF_PNI,
|
||||
e164 = SELF_E164,
|
||||
profileKey = SELF_PROFILE_KEY,
|
||||
profileSharing = true
|
||||
)
|
||||
|
||||
content()
|
||||
|
||||
val startingMainData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
|
||||
val startingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
|
||||
|
||||
val exported: ByteArray = BackupRepository.export()
|
||||
BackupRepository.import(length = exported.size.toLong(), inputStreamFactory = { ByteArrayInputStream(exported) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
|
||||
|
||||
val endingData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
|
||||
val endingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
|
||||
|
||||
assertDatabaseMatches(startingMainData, endingData)
|
||||
assertDatabaseMatches(startingKeyValueData, endingKeyValueData)
|
||||
}
|
||||
|
||||
private fun individualChat(aci: ACI, givenName: String, familyName: String? = null, init: IndividualChatCreator.() -> Unit) {
|
||||
val recipientId = individualRecipient(aci = aci, givenName = givenName, familyName = familyName, profileSharing = true)
|
||||
|
||||
val threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
|
||||
|
||||
IndividualChatCreator(SignalDatabase.rawDatabase, recipientId, threadId).init()
|
||||
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
}
|
||||
|
||||
private fun individualRecipient(
|
||||
aci: ACI? = null,
|
||||
pni: PNI? = null,
|
||||
e164: String? = null,
|
||||
givenName: String? = null,
|
||||
familyName: String? = null,
|
||||
username: String? = null,
|
||||
hidden: Boolean = false,
|
||||
registeredState: RecipientTable.RegisteredState = RecipientTable.RegisteredState.UNKNOWN,
|
||||
profileKey: ProfileKey? = null,
|
||||
profileSharing: Boolean = false,
|
||||
hideStory: Boolean = false
|
||||
): RecipientId {
|
||||
check(aci != null || pni != null || e164 != null)
|
||||
|
||||
val recipientId = SignalDatabase.recipients.getAndPossiblyMerge(aci, pni, e164, pniVerified = true, changeSelf = true)
|
||||
|
||||
if (givenName != null || familyName != null) {
|
||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts(givenName, familyName))
|
||||
}
|
||||
|
||||
if (username != null) {
|
||||
SignalDatabase.recipients.setUsername(recipientId, username)
|
||||
}
|
||||
|
||||
if (registeredState == RecipientTable.RegisteredState.REGISTERED) {
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci ?: pni!!)
|
||||
} else if (registeredState == RecipientTable.RegisteredState.NOT_REGISTERED) {
|
||||
SignalDatabase.recipients.markUnregistered(recipientId)
|
||||
}
|
||||
|
||||
if (profileKey != null) {
|
||||
SignalDatabase.recipients.setProfileKey(recipientId, profileKey)
|
||||
}
|
||||
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, profileSharing)
|
||||
SignalDatabase.recipients.setHideStory(recipientId, hideStory)
|
||||
|
||||
if (hidden) {
|
||||
SignalDatabase.recipients.markHidden(recipientId)
|
||||
}
|
||||
|
||||
return recipientId
|
||||
}
|
||||
|
||||
private inner class IndividualChatCreator(
|
||||
private val db: SQLiteDatabase,
|
||||
private val recipientId: RecipientId,
|
||||
private val threadId: Long
|
||||
) {
|
||||
fun standardMessage(
|
||||
outgoing: Boolean,
|
||||
sentTimestamp: Long = System.currentTimeMillis(),
|
||||
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
|
||||
serverTimestamp: Long = sentTimestamp,
|
||||
body: String? = null,
|
||||
read: Boolean = true,
|
||||
quotes: Long? = null,
|
||||
quoteTargetMissing: Boolean = false,
|
||||
randomMention: Boolean = false,
|
||||
randomStyling: Boolean = false
|
||||
): Long {
|
||||
return db.insertMessage(
|
||||
from = if (outgoing) Recipient.self().id else recipientId,
|
||||
to = if (outgoing) recipientId else Recipient.self().id,
|
||||
outgoing = outgoing,
|
||||
threadId = threadId,
|
||||
sentTimestamp = sentTimestamp,
|
||||
receivedTimestamp = receivedTimestamp,
|
||||
serverTimestamp = serverTimestamp,
|
||||
body = body,
|
||||
read = read,
|
||||
quotes = quotes,
|
||||
quoteTargetMissing = quoteTargetMissing,
|
||||
randomMention = randomMention,
|
||||
randomStyling = randomStyling
|
||||
)
|
||||
}
|
||||
|
||||
fun remoteDeletedMessage(
|
||||
outgoing: Boolean,
|
||||
sentTimestamp: Long = System.currentTimeMillis(),
|
||||
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
|
||||
serverTimestamp: Long = sentTimestamp
|
||||
): Long {
|
||||
return db.insertMessage(
|
||||
from = if (outgoing) Recipient.self().id else recipientId,
|
||||
to = if (outgoing) recipientId else Recipient.self().id,
|
||||
outgoing = outgoing,
|
||||
threadId = threadId,
|
||||
sentTimestamp = sentTimestamp,
|
||||
receivedTimestamp = receivedTimestamp,
|
||||
serverTimestamp = serverTimestamp,
|
||||
remoteDeleted = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.insertMessage(
|
||||
from: RecipientId,
|
||||
to: RecipientId,
|
||||
outgoing: Boolean,
|
||||
threadId: Long,
|
||||
sentTimestamp: Long = System.currentTimeMillis(),
|
||||
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
|
||||
serverTimestamp: Long = sentTimestamp,
|
||||
body: String? = null,
|
||||
read: Boolean = true,
|
||||
quotes: Long? = null,
|
||||
quoteTargetMissing: Boolean = false,
|
||||
randomMention: Boolean = false,
|
||||
randomStyling: Boolean = false,
|
||||
remoteDeleted: Boolean = false
|
||||
): Long {
|
||||
val type = if (outgoing) {
|
||||
MessageTypes.BASE_SENT_TYPE
|
||||
} else {
|
||||
MessageTypes.BASE_INBOX_TYPE
|
||||
} or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT
|
||||
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(MessageTable.DATE_SENT, sentTimestamp)
|
||||
contentValues.put(MessageTable.DATE_RECEIVED, receivedTimestamp)
|
||||
contentValues.put(MessageTable.FROM_RECIPIENT_ID, from.serialize())
|
||||
contentValues.put(MessageTable.TO_RECIPIENT_ID, to.serialize())
|
||||
contentValues.put(MessageTable.THREAD_ID, threadId)
|
||||
contentValues.put(MessageTable.BODY, body)
|
||||
contentValues.put(MessageTable.TYPE, type)
|
||||
contentValues.put(MessageTable.READ, if (read) 1 else 0)
|
||||
|
||||
if (!outgoing) {
|
||||
contentValues.put(MessageTable.DATE_SERVER, serverTimestamp)
|
||||
}
|
||||
|
||||
if (remoteDeleted) {
|
||||
contentValues.put(MessageTable.REMOTE_DELETED, 1)
|
||||
return this
|
||||
.insertInto(MessageTable.TABLE_NAME)
|
||||
.values(contentValues)
|
||||
.run()
|
||||
}
|
||||
|
||||
if (quotes != null) {
|
||||
val quoteDetails = this.getQuoteDetailsFor(quotes)
|
||||
contentValues.put(MessageTable.QUOTE_ID, if (quoteTargetMissing) MessageTable.QUOTE_TARGET_MISSING_ID else quoteDetails.quotedSentTimestamp)
|
||||
contentValues.put(MessageTable.QUOTE_AUTHOR, quoteDetails.authorId.serialize())
|
||||
contentValues.put(MessageTable.QUOTE_BODY, quoteDetails.body)
|
||||
contentValues.put(MessageTable.QUOTE_BODY_RANGES, quoteDetails.bodyRanges)
|
||||
contentValues.put(MessageTable.QUOTE_TYPE, quoteDetails.type)
|
||||
contentValues.put(MessageTable.QUOTE_MISSING, quoteTargetMissing.toInt())
|
||||
}
|
||||
|
||||
if (body != null && (randomMention || randomStyling)) {
|
||||
val ranges: MutableList<BodyRangeList.BodyRange> = mutableListOf()
|
||||
|
||||
if (randomMention) {
|
||||
ranges += BodyRangeList.BodyRange(
|
||||
start = 0,
|
||||
length = Random.nextInt(body.length),
|
||||
mentionUuid = if (outgoing) Recipient.resolved(to).requireAci().toString() else Recipient.resolved(from).requireAci().toString()
|
||||
)
|
||||
}
|
||||
|
||||
if (randomStyling) {
|
||||
ranges += BodyRangeList.BodyRange(
|
||||
start = 0,
|
||||
length = Random.nextInt(body.length),
|
||||
style = BodyRangeList.BodyRange.Style.fromValue(Random.nextInt(BodyRangeList.BodyRange.Style.values().size))
|
||||
)
|
||||
}
|
||||
|
||||
contentValues.put(MessageTable.MESSAGE_RANGES, BodyRangeList(ranges = ranges).encode())
|
||||
}
|
||||
|
||||
return this
|
||||
.insertInto(MessageTable.TABLE_NAME)
|
||||
.values(contentValues)
|
||||
.run()
|
||||
}
|
||||
|
||||
private fun assertDatabaseMatches(expected: DatabaseData, actual: DatabaseData) {
|
||||
assert(expected.keys.size == actual.keys.size) { "Mismatched table count! Expected: ${expected.keys} || Actual: ${actual.keys}" }
|
||||
assert(expected.keys.containsAll(actual.keys)) { "Table names differ! Expected: ${expected.keys} || Actual: ${actual.keys}" }
|
||||
|
||||
val tablesToCheck = expected.keys.filter { !IGNORED_TABLES.contains(it) }
|
||||
|
||||
for (table in tablesToCheck) {
|
||||
val expectedTable: List<Map<String, Any?>> = expected[table]!!
|
||||
val actualTable: List<Map<String, Any?>> = actual[table]!!
|
||||
|
||||
assert(expectedTable.size == actualTable.size) { "Mismatched number of rows for table '$table'! Expected: ${expectedTable.size} || Actual: ${actualTable.size}\n $actualTable" }
|
||||
|
||||
val expectedFiltered: List<Map<String, Any?>> = expectedTable.withoutExcludedColumns(IGNORED_COLUMNS[table])
|
||||
val actualFiltered: List<Map<String, Any?>> = actualTable.withoutExcludedColumns(IGNORED_COLUMNS[table])
|
||||
|
||||
assert(contentEquals(expectedFiltered, actualFiltered)) { "Data did not match for table '$table'!\n${prettyDiff(expectedFiltered, actualFiltered)}" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun contentEquals(expectedRows: List<Map<String, Any?>>, actualRows: List<Map<String, Any?>>): Boolean {
|
||||
if (expectedRows == actualRows) {
|
||||
return true
|
||||
}
|
||||
|
||||
assert(expectedRows.size == actualRows.size)
|
||||
|
||||
for (i in expectedRows.indices) {
|
||||
val expectedRow = expectedRows[i]
|
||||
val actualRow = actualRows[i]
|
||||
|
||||
for (key in expectedRow.keys) {
|
||||
val expectedValue = expectedRow[key]
|
||||
val actualValue = actualRow[key]
|
||||
|
||||
if (!contentEquals(expectedValue, actualValue)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun contentEquals(lhs: Any?, rhs: Any?): Boolean {
|
||||
return if (lhs is ByteArray && rhs is ByteArray) {
|
||||
lhs.contentEquals(rhs)
|
||||
} else {
|
||||
lhs == rhs
|
||||
}
|
||||
}
|
||||
|
||||
private fun prettyDiff(expectedRows: List<Map<String, Any?>>, actualRows: List<Map<String, Any?>>): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
assert(expectedRows.size == actualRows.size)
|
||||
|
||||
for (i in expectedRows.indices) {
|
||||
val expectedRow = expectedRows[i]
|
||||
val actualRow = actualRows[i]
|
||||
var describedRow = false
|
||||
|
||||
for (key in expectedRow.keys) {
|
||||
val expectedValue = expectedRow[key]
|
||||
val actualValue = actualRow[key]
|
||||
|
||||
if (!contentEquals(expectedValue, actualValue)) {
|
||||
if (!describedRow) {
|
||||
builder.append("-- ROW ${i + 1}\n")
|
||||
describedRow = true
|
||||
}
|
||||
builder.append(" [$key] Expected: ${expectedValue.prettyPrint()} || Actual: ${actualValue.prettyPrint()} \n")
|
||||
}
|
||||
}
|
||||
|
||||
if (describedRow) {
|
||||
builder.append("\n")
|
||||
builder.append("Expected: $expectedRow\n")
|
||||
builder.append("Actual: $actualRow\n")
|
||||
}
|
||||
}
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun Any?.prettyPrint(): String {
|
||||
return when (this) {
|
||||
is ByteArray -> "Bytes(${Hex.toString(this)})"
|
||||
else -> this.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Map<String, Any?>>.withoutExcludedColumns(ignored: Set<String>?): List<Map<String, Any?>> {
|
||||
return if (ignored != null) {
|
||||
this.map { row ->
|
||||
row.filterKeys { !ignored.contains(it) }
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.getQuoteDetailsFor(messageId: Long): QuoteDetails {
|
||||
return this
|
||||
.select(
|
||||
MessageTable.DATE_SENT,
|
||||
MessageTable.FROM_RECIPIENT_ID,
|
||||
MessageTable.BODY,
|
||||
MessageTable.MESSAGE_RANGES
|
||||
)
|
||||
.from(MessageTable.TABLE_NAME)
|
||||
.where("${MessageTable.ID} = ?", messageId)
|
||||
.run()
|
||||
.readToSingleObject { cursor ->
|
||||
QuoteDetails(
|
||||
quotedSentTimestamp = cursor.requireLong(MessageTable.DATE_SENT),
|
||||
authorId = RecipientId.from(cursor.requireLong(MessageTable.FROM_RECIPIENT_ID)),
|
||||
body = cursor.requireString(MessageTable.BODY),
|
||||
bodyRanges = cursor.requireBlob(MessageTable.MESSAGE_RANGES),
|
||||
type = QuoteModel.Type.NORMAL.code
|
||||
)
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.readAllContents(): DatabaseData {
|
||||
return SqlUtil.getAllTables(this).associateWith { table -> this.getAllTableData(table) }
|
||||
}
|
||||
|
||||
private fun SQLiteDatabase.getAllTableData(table: String): List<Map<String, Any?>> {
|
||||
return this
|
||||
.select()
|
||||
.from(table)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val map: MutableMap<String, Any?> = mutableMapOf()
|
||||
|
||||
for (i in 0 until cursor.columnCount) {
|
||||
val column = cursor.getColumnName(i)
|
||||
|
||||
when (cursor.getType(i)) {
|
||||
Cursor.FIELD_TYPE_INTEGER -> map[column] = cursor.getInt(i)
|
||||
Cursor.FIELD_TYPE_FLOAT -> map[column] = cursor.getFloat(i)
|
||||
Cursor.FIELD_TYPE_STRING -> map[column] = cursor.getString(i)
|
||||
Cursor.FIELD_TYPE_BLOB -> map[column] = cursor.getBlob(i)
|
||||
Cursor.FIELD_TYPE_NULL -> map[column] = null
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
private data class QuoteDetails(
|
||||
val quotedSentTimestamp: Long,
|
||||
val authorId: RecipientId,
|
||||
val body: String?,
|
||||
val bodyRanges: ByteArray?,
|
||||
val type: Int
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -230,8 +230,6 @@ class ChangeNumberViewModelTest {
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
MockProvider.mockGetRegistrationLockStringFlow()
|
||||
|
||||
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) },
|
||||
@@ -318,8 +316,6 @@ class ChangeNumberViewModelTest {
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
MockProvider.mockGetRegistrationLockStringFlow()
|
||||
|
||||
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) },
|
||||
|
||||
@@ -9,8 +9,9 @@ import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
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
|
||||
@@ -64,7 +65,8 @@ class ConversationItemPreviewer {
|
||||
attachment()
|
||||
}
|
||||
|
||||
val message = IncomingMediaMessage(
|
||||
val message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
@@ -73,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)
|
||||
}
|
||||
@@ -83,7 +85,8 @@ class ConversationItemPreviewer {
|
||||
attachment()
|
||||
}
|
||||
|
||||
val message = IncomingMediaMessage(
|
||||
val message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
@@ -92,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) {
|
||||
@@ -144,6 +147,7 @@ class ConversationItemPreviewer {
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
0,
|
||||
Optional.of("/not-there.jpg"),
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Observer
|
||||
import com.bumptech.glide.RequestManager
|
||||
import io.mockk.mockk
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
@@ -29,7 +30,6 @@ 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.mms.GlideRequests
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
@@ -47,7 +47,6 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
@@ -69,7 +68,6 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.END
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
@@ -91,7 +89,6 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.START
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(prev),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
@@ -115,7 +112,6 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.MIDDLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
@@ -137,7 +133,6 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
@@ -159,7 +154,6 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(prev),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
@@ -183,7 +177,6 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
@@ -210,14 +203,15 @@ class V2ConversationItemShapeTest {
|
||||
|
||||
private val colorizer = Colorizer()
|
||||
|
||||
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.STANDARD
|
||||
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 glideRequests: GlideRequests = mockk()
|
||||
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
|
||||
|
||||
@@ -328,5 +322,11 @@ class V2ConversationItemShapeTest {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,18 +51,16 @@ 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)
|
||||
}
|
||||
@@ -79,18 +77,16 @@ 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)
|
||||
}
|
||||
@@ -121,15 +117,14 @@ class AttachmentTableTest {
|
||||
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
|
||||
|
||||
// WHEN
|
||||
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData), false)
|
||||
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData))
|
||||
|
||||
// THEN
|
||||
val previousInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(previousDatabaseAttachmentId, AttachmentTable.DATA)!!
|
||||
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
||||
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
||||
val previousInfo = SignalDatabase.attachments.getDataFileInfo(previousDatabaseAttachmentId)!!
|
||||
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
|
||||
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
|
||||
|
||||
assertNotEquals(standardInfo, highInfo)
|
||||
standardInfo.file assertIs previousInfo.file
|
||||
highInfo.file assertIsNot standardInfo.file
|
||||
highInfo.file.exists() assertIs true
|
||||
}
|
||||
@@ -158,9 +153,9 @@ class AttachmentTableTest {
|
||||
val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload)
|
||||
|
||||
// THEN
|
||||
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
||||
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
||||
val secondHighInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(secondHighDatabaseAttachment.attachmentId, AttachmentTable.DATA)!!
|
||||
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
|
||||
|
||||
@@ -0,0 +1,804 @@
|
||||
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.PointerAttachment
|
||||
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.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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
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.cdnNumber, rhsAttachment.cdnNumber)
|
||||
}
|
||||
|
||||
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.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,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(content: TestContext.() -> Unit) {
|
||||
SignalDatabase.attachments.deleteAllAttachments()
|
||||
val context = TestContext()
|
||||
context.content()
|
||||
}
|
||||
}
|
||||
@@ -214,6 +214,175 @@ class CallTableTest {
|
||||
assertEquals(CallTable.Event.JOINED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
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()
|
||||
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.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"
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 +33,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
|
||||
@@ -293,22 +293,22 @@ class GroupTableTest {
|
||||
|
||||
private fun insertPushGroup(
|
||||
members: List<DecryptedMember> = listOf(
|
||||
DecryptedMember.newBuilder()
|
||||
.setAciBytes(harness.self.requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(harness.self.requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
.build(),
|
||||
DecryptedMember.newBuilder()
|
||||
.setAciBytes(Recipient.resolved(harness.others[0]).requireAci().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()
|
||||
.members(members)
|
||||
.revision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)!!
|
||||
@@ -317,23 +317,23 @@ class GroupTableTest {
|
||||
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
|
||||
val selfMember: DecryptedMember = DecryptedMember.newBuilder()
|
||||
.setAciBytes(harness.self.requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
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.newBuilder()
|
||||
.setAciBytes(Recipient.resolved(id).requireAci().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
DecryptedMember.Builder()
|
||||
.aciBytes(Recipient.resolved(id).requireAci().toByteString())
|
||||
.joinedAtRevision(0)
|
||||
.role(Member.Role.DEFAULT)
|
||||
.build()
|
||||
}
|
||||
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(listOf(selfMember) + otherMembers)
|
||||
.setRevision(0)
|
||||
val decryptedGroupState = DecryptedGroup.Builder()
|
||||
.members(listOf(selfMember) + otherMembers)
|
||||
.revision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)!!
|
||||
|
||||
@@ -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.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
|
||||
class LogDatabaseTest {
|
||||
|
||||
private val db: LogDatabase = LogDatabase.getInstance(ApplicationDependencies.getApplication())
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,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))
|
||||
@@ -62,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))
|
||||
|
||||
@@ -76,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 = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
|
||||
@@ -96,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 = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
|
||||
@@ -115,13 +115,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 = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
@@ -140,13 +140,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 = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
@@ -165,13 +165,13 @@ 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 = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
giftBadge = GiftBadge()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
|
||||
@@ -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
|
||||
@@ -55,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,7 +13,7 @@ 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.ServiceId.ACI
|
||||
@@ -73,7 +73,8 @@ class MmsTableTest_stories {
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = sender,
|
||||
sentTimeMillis = 2,
|
||||
serverTimeMillis = 2,
|
||||
@@ -95,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,
|
||||
@@ -122,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,
|
||||
@@ -154,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,
|
||||
@@ -168,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,
|
||||
@@ -213,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,
|
||||
@@ -321,7 +327,8 @@ class MmsTableTest_stories {
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
IncomingMediaMessage(
|
||||
IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = myStory.id,
|
||||
sentTimeMillis = 201,
|
||||
serverTimeMillis = 201,
|
||||
|
||||
@@ -11,8 +11,6 @@ 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.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.UUID
|
||||
@@ -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,8 +165,6 @@ 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)
|
||||
@@ -185,12 +181,10 @@ 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.getByAci(ACI_A).get()
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@ 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
|
||||
@@ -34,14 +37,10 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
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.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
@@ -57,7 +56,6 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
SignalStore.account().setE164(E164_SELF)
|
||||
SignalStore.account().setAci(ACI_SELF)
|
||||
SignalStore.account().setPni(PNI_SELF)
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,6 +107,18 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
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
|
||||
@@ -142,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)
|
||||
@@ -201,6 +236,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
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") {
|
||||
@@ -502,6 +539,18 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
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)
|
||||
@@ -658,6 +707,8 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
expectDeleted()
|
||||
expect(E164_A, PNI_A, ACI_A)
|
||||
|
||||
expectPniVerified()
|
||||
}
|
||||
|
||||
test("merge, e164+pni & aci, pni session, pni verified") {
|
||||
@@ -670,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") {
|
||||
@@ -724,6 +776,18 @@ 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)
|
||||
@@ -736,6 +800,17 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
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)
|
||||
@@ -789,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)!!
|
||||
@@ -838,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)!!
|
||||
@@ -883,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))
|
||||
@@ -911,12 +986,30 @@ 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 {
|
||||
@@ -983,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}")
|
||||
@@ -1002,6 +1099,7 @@ 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
|
||||
@@ -1153,6 +1251,24 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
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 id: Long = SignalDatabase.rawDatabase.insert(
|
||||
RecipientTable.TABLE_NAME,
|
||||
@@ -1228,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
|
||||
}
|
||||
@@ -1246,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
|
||||
}
|
||||
|
||||
@@ -18,12 +18,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.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName", "TestFunctionName")
|
||||
@@ -272,13 +270,29 @@ 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++
|
||||
return IncomingMessage.groupUpdate(
|
||||
from = sender,
|
||||
timestamp = wallClock,
|
||||
groupId = groupId,
|
||||
groupContext = groupContext,
|
||||
serverGuid = null
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -13,9 +13,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 +23,25 @@ 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.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 org.whispersystems.signalservice.internal.configuration.SignalSvr2Url
|
||||
import java.security.KeyStore
|
||||
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) : ApplicationDependencies.Provider by default {
|
||||
|
||||
private val serviceTrustStore: TrustStore
|
||||
private val uncensoredConfiguration: SignalServiceConfiguration
|
||||
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
|
||||
private val keyBackupService: KeyBackupService
|
||||
private val recipientCache: LiveRecipientCache
|
||||
|
||||
init {
|
||||
@@ -80,7 +73,6 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
||||
0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT))
|
||||
),
|
||||
signalKeyBackupServiceUrls = arrayOf(SignalKeyBackupServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
signalStorageUrls = arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
signalCdsiUrls = arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
signalSvr2Urls = arrayOf(SignalSvr2Url(baseUrl, serviceTrustStore, "localhost", ConnectionSpec.CLEARTEXT)),
|
||||
@@ -88,7 +80,8 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
||||
dns = Optional.of(SignalServiceNetworkAccess.DNS),
|
||||
signalProxy = Optional.empty(),
|
||||
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
|
||||
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS)
|
||||
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS),
|
||||
backupServerPublicParams = Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS)
|
||||
)
|
||||
|
||||
serviceNetworkAccessMock = mock {
|
||||
@@ -97,8 +90,6 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
||||
on { uncensoredConfiguration } doReturn uncensoredConfiguration
|
||||
}
|
||||
|
||||
keyBackupService = mock()
|
||||
|
||||
recipientCache = LiveRecipientCache(application) { r -> r.run() }
|
||||
}
|
||||
|
||||
@@ -106,10 +97,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,181 +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.Delete
|
||||
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.account().usernameOutOfSync = false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoLocalUsername_whenICheckUsernameIsInSync_thenIExpectNoFailures() {
|
||||
// GIVEN
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Delete("/v1/accounts/username_hash") { MockResponse().success() }
|
||||
)
|
||||
|
||||
// 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.account().usernameOutOfSync)
|
||||
}
|
||||
|
||||
@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.account().usernameOutOfSync)
|
||||
}
|
||||
|
||||
@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.account().usernameOutOfSync)
|
||||
}
|
||||
|
||||
@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.account().usernameOutOfSync)
|
||||
}
|
||||
}
|
||||
@@ -22,8 +22,9 @@ 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.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage
|
||||
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
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
.setDestinationServiceId(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)
|
||||
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()
|
||||
.setDestinationServiceId(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)
|
||||
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,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
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.database.model.toProtoByteString
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
|
||||
@@ -15,8 +15,8 @@ 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.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -41,9 +41,9 @@ class MessageContentProcessor__recipientStatusTest {
|
||||
@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.newBuilder().setRevision(0).setMasterKey(masterKey.serialize().toProtoByteString()).build()
|
||||
val groupContextV2 = GroupContextV2.Builder().revision(0).masterKey(masterKey.serialize().toByteString()).build()
|
||||
|
||||
val initialTextMessage = DataMessage.newBuilder().buildWith {
|
||||
val initialTextMessage = DataMessage.Builder().buildWith {
|
||||
body = MessageContentFuzzer.string()
|
||||
groupV2 = groupContextV2
|
||||
timestamp = envelopeTimestamp
|
||||
@@ -52,7 +52,7 @@ class MessageContentProcessor__recipientStatusTest {
|
||||
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),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ class MessageContentProcessor__recipientStatusTest {
|
||||
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),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ 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
|
||||
@@ -26,7 +25,7 @@ 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.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketMessage
|
||||
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
|
||||
import java.util.regex.Pattern
|
||||
@@ -93,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 {
|
||||
@@ -190,7 +189,7 @@ class MessageProcessingPerformanceTest {
|
||||
path = "/api/v1/message",
|
||||
id = Random(System.currentTimeMillis()).nextLong(),
|
||||
headers = listOf("X-Signal-Timestamp: ${this.timestamp}"),
|
||||
body = this.toByteArray().toByteString()
|
||||
body = this.encodeByteString()
|
||||
)
|
||||
).encodeByteString()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* 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 io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SyncMessageProcessorTest_readSyncs {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
private lateinit var alice: RecipientId
|
||||
private lateinit var bob: RecipientId
|
||||
private lateinit var group: GroupTestingUtils.TestGroupInfo
|
||||
private lateinit var processor: MessageContentProcessor
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
alice = harness.others[0]
|
||||
bob = harness.others[1]
|
||||
group = harness.group!!
|
||||
|
||||
processor = MessageContentProcessor(harness.context)
|
||||
|
||||
val threadIdSlot = slot<Long>()
|
||||
mockkStatic(ThreadUpdateJob::class)
|
||||
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
|
||||
SignalDatabase.threads.update(threadIdSlot.captured, false)
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic(ThreadUpdateJob::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadMessage() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp, alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadMessageMissingTimestamp() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
messageHelper.incomingText().timestamp
|
||||
val message2Timestamp = messageHelper.incomingText().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message2Timestamp)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEdits() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText().timestamp
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp)
|
||||
|
||||
val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp
|
||||
val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp
|
||||
|
||||
val message2Timestamp = messageHelper.incomingMedia().timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(alice to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSynchronizeReadWithEditsInGroup() {
|
||||
val messageHelper = MessageHelper()
|
||||
|
||||
val message1Timestamp = messageHelper.incomingText(sender = alice, destination = group.recipientId).timestamp
|
||||
|
||||
messageHelper.syncReadMessage(alice to message1Timestamp)
|
||||
|
||||
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = alice, destination = group.recipientId).timestamp
|
||||
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = alice, destination = group.recipientId).timestamp
|
||||
|
||||
val message2Timestamp = messageHelper.incomingMedia(sender = bob, destination = group.recipientId).timestamp
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(group.recipientId)!!
|
||||
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 2
|
||||
|
||||
messageHelper.syncReadMessage(bob to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
|
||||
|
||||
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
|
||||
threadRecord.unreadCount assertIs 0
|
||||
}
|
||||
|
||||
private inner class MessageHelper(var startTime: Long = System.currentTimeMillis()) {
|
||||
|
||||
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.fuzzStickerMediaMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
|
||||
startTime += 1000
|
||||
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.editTextMessage(
|
||||
targetTimestamp = targetTimestamp,
|
||||
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
|
||||
sentTimestamp = messageData.timestamp,
|
||||
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
|
||||
).dataMessage!!
|
||||
),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(
|
||||
source = sender,
|
||||
destination = harness.self.id,
|
||||
groupId = if (destination == group.recipientId) group.groupId else null
|
||||
),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
|
||||
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
|
||||
startTime += 1000
|
||||
val messageData = MessageData(timestamp = startTime)
|
||||
|
||||
processor.process(
|
||||
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
|
||||
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
|
||||
serverDeliveredTimestamp = messageData.timestamp + 10
|
||||
)
|
||||
|
||||
return messageData
|
||||
}
|
||||
}
|
||||
|
||||
private data class MessageData(val serverGuid: UUID = UUID.randomUUID(), val timestamp: Long)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ 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.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
|
||||
class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(context) {
|
||||
companion object {
|
||||
@@ -19,9 +20,9 @@ class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(
|
||||
fun endTag(timestamp: Long) = "$timestamp end"
|
||||
}
|
||||
|
||||
override fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean, localMetric: SignalLocalMetrics.MessageReceive?) {
|
||||
Log.d(TAG, startTag(envelope.timestamp))
|
||||
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))
|
||||
Log.d(TAG, endTag(envelope.timestamp!!))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,14 +6,12 @@ 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.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
@@ -29,11 +27,10 @@ class ContactRecordProcessorTest {
|
||||
SignalStore.account().setE164(E164_SELF)
|
||||
SignalStore.account().setAci(ACI_SELF)
|
||||
SignalStore.account().setPni(PNI_SELF)
|
||||
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
|
||||
}
|
||||
|
||||
@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)
|
||||
@@ -69,6 +66,35 @@ class ContactRecordProcessorTest {
|
||||
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)
|
||||
assertNotEquals(byAci, byE164)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun process_splitContact_doNotSplitIfAciRecordIsRegistered() {
|
||||
// GIVEN
|
||||
@@ -113,7 +139,7 @@ class ContactRecordProcessorTest {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ 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.
|
||||
@@ -40,7 +40,7 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
|
||||
ApplicationDependencies.getIncomingMessageObserver()
|
||||
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
|
||||
?.mapNotNull { it.run() }
|
||||
?.forEach { ApplicationDependencies.getJobManager().add(it) }
|
||||
?.forEach { it.enqueue() }
|
||||
|
||||
bufferedStore.flushToDisk()
|
||||
val end = System.currentTimeMillis()
|
||||
|
||||
@@ -31,11 +31,10 @@ 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 org.whispersystems.signalservice.internal.push.Envelope
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.UnsupportedOperationException
|
||||
|
||||
/**
|
||||
* Welcome to Bob's Client.
|
||||
@@ -61,7 +60,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)
|
||||
@@ -72,10 +71,10 @@ 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) {
|
||||
fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) {
|
||||
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator())
|
||||
cipher.decrypt(envelope, serverDeliveredTimestamp)
|
||||
}
|
||||
@@ -166,7 +165,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
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()
|
||||
|
||||
@@ -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.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
|
||||
|
||||
@@ -52,9 +54,9 @@ object FakeClientHelpers {
|
||||
}
|
||||
|
||||
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 +66,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)
|
||||
.setDestinationServiceId(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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -9,6 +10,7 @@ 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
|
||||
|
||||
/**
|
||||
@@ -16,19 +18,19 @@ import kotlin.random.Random
|
||||
*/
|
||||
object GroupTestingUtils {
|
||||
fun member(aci: ACI, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setAciBytes(aci.toByteString())
|
||||
.setJoinedAtRevision(revision)
|
||||
.setRole(role)
|
||||
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.newBuilder()
|
||||
.addAllMembers(members.toList())
|
||||
.setRevision(revision)
|
||||
.setTitle(MessageContentFuzzer.string())
|
||||
val decryptedGroupState = DecryptedGroup.Builder()
|
||||
.members(members.toList())
|
||||
.revision(revision)
|
||||
.title(MessageContentFuzzer.string())
|
||||
.build()
|
||||
|
||||
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!!
|
||||
@@ -46,5 +48,8 @@ object GroupTestingUtils {
|
||||
return member(aci = requireAci())
|
||||
}
|
||||
|
||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId)
|
||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) {
|
||||
val groupV2Context: GroupContextV2
|
||||
get() = GroupContextV2(masterKey = masterKey.serialize().toByteString(), revision = 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
|
||||
import org.thoughtcrime.securesms.messages.TestMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
|
||||
import org.whispersystems.signalservice.internal.push.AttachmentPointer
|
||||
import org.whispersystems.signalservice.internal.push.BodyRange
|
||||
import org.whispersystems.signalservice.internal.push.Content
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.EditMessage
|
||||
import org.whispersystems.signalservice.internal.push.Envelope
|
||||
import org.whispersystems.signalservice.internal.push.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.push.SyncMessage
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
@@ -34,22 +34,22 @@ object MessageContentFuzzer {
|
||||
/**
|
||||
* Create an [Envelope].
|
||||
*/
|
||||
fun envelope(timestamp: Long): Envelope {
|
||||
return Envelope.newBuilder()
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 5)
|
||||
.setServerGuidBytes(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
fun envelope(timestamp: Long, serverGuid: UUID = UUID.randomUUID()): Envelope {
|
||||
return Envelope.Builder()
|
||||
.timestamp(timestamp)
|
||||
.serverTimestamp(timestamp + 5)
|
||||
.serverGuid(serverGuid.toString())
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create metadata to match an [Envelope].
|
||||
*/
|
||||
fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata {
|
||||
fun envelopeMetadata(source: RecipientId, destination: RecipientId, sourceDeviceId: Int = 1, groupId: GroupId.V2? = null): EnvelopeMetadata {
|
||||
return EnvelopeMetadata(
|
||||
sourceServiceId = Recipient.resolved(source).requireServiceId(),
|
||||
sourceE164 = null,
|
||||
sourceDeviceId = 1,
|
||||
sourceDeviceId = sourceDeviceId,
|
||||
sealedSender = true,
|
||||
groupId = groupId?.decodedId,
|
||||
destinationServiceId = Recipient.resolved(destination).requireServiceId()
|
||||
@@ -61,21 +61,24 @@ object MessageContentFuzzer {
|
||||
* - An expire timer value
|
||||
* - Bold style body ranges
|
||||
*/
|
||||
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
timestamp = sentTimestamp
|
||||
body = string()
|
||||
if (random.nextBoolean()) {
|
||||
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
|
||||
}
|
||||
if (random.nextBoolean()) {
|
||||
addBodyRanges(
|
||||
SignalServiceProtos.BodyRange.newBuilder().buildWith {
|
||||
start = 0
|
||||
length = 1
|
||||
style = SignalServiceProtos.BodyRange.Style.BOLD
|
||||
}
|
||||
bodyRanges(
|
||||
listOf(
|
||||
BodyRange.Builder().buildWith {
|
||||
start = 0
|
||||
length = 1
|
||||
style = BodyRange.Style.BOLD
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
if (groupContextV2 != null) {
|
||||
@@ -86,6 +89,20 @@ object MessageContentFuzzer {
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an edit message.
|
||||
*/
|
||||
fun editTextMessage(targetTimestamp: Long, editedDataMessage: DataMessage): Content {
|
||||
return Content.Builder()
|
||||
.editMessage(
|
||||
EditMessage.Builder().buildWith {
|
||||
targetSentTimestamp = targetTimestamp
|
||||
dataMessage = editedDataMessage
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sync sent text message for the given [DataMessage].
|
||||
*/
|
||||
@@ -95,16 +112,16 @@ object MessageContentFuzzer {
|
||||
recipientUpdate: Boolean = false
|
||||
): Content {
|
||||
return Content
|
||||
.newBuilder()
|
||||
.setSyncMessage(
|
||||
SyncMessage.newBuilder().buildWith {
|
||||
sent = SyncMessage.Sent.newBuilder().buildWith {
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage.Builder().buildWith {
|
||||
sent = SyncMessage.Sent.Builder().buildWith {
|
||||
timestamp = textMessage.timestamp
|
||||
message = textMessage
|
||||
isRecipientUpdate = recipientUpdate
|
||||
addAllUnidentifiedStatus(
|
||||
unidentifiedStatus(
|
||||
deliveredTo.map {
|
||||
SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder().buildWith {
|
||||
SyncMessage.Sent.UnidentifiedDeliveryStatus.Builder().buildWith {
|
||||
destinationServiceId = Recipient.resolved(it).requireServiceId().toString()
|
||||
unidentified = true
|
||||
}
|
||||
@@ -115,6 +132,24 @@ object MessageContentFuzzer {
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sync reads message for the given [RecipientId] and message timestamp pairings.
|
||||
*/
|
||||
fun syncReadsMessage(timestamps: List<Pair<RecipientId, Long>>): Content {
|
||||
return Content
|
||||
.Builder()
|
||||
.syncMessage(
|
||||
SyncMessage.Builder().buildWith {
|
||||
read = timestamps.map { (senderId, timestamp) ->
|
||||
SyncMessage.Read.Builder().buildWith {
|
||||
this.senderAci = Recipient.resolved(senderId).requireAci().toString()
|
||||
this.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that may be:
|
||||
* - A text body
|
||||
@@ -123,9 +158,9 @@ object MessageContentFuzzer {
|
||||
* - A message with 0-2 attachment pointers and may contain a text body
|
||||
*/
|
||||
fun fuzzMediaMessageWithBody(quoteAble: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
if (random.nextBoolean()) {
|
||||
body = string()
|
||||
}
|
||||
@@ -133,28 +168,28 @@ object MessageContentFuzzer {
|
||||
if (random.nextBoolean() && quoteAble.isNotEmpty()) {
|
||||
body = string()
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.newBuilder().buildWith {
|
||||
quote = DataMessage.Quote.Builder().buildWith {
|
||||
id = quoted.envelope.timestamp
|
||||
authorAci = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage.body
|
||||
addAllAttachments(quoted.content.dataMessage.attachmentsList)
|
||||
addAllBodyRanges(quoted.content.dataMessage.bodyRangesList)
|
||||
text = quoted.content.dataMessage?.body
|
||||
attachments(quoted.content.dataMessage?.attachments ?: emptyList())
|
||||
bodyRanges(quoted.content.dataMessage?.bodyRanges ?: emptyList())
|
||||
type = DataMessage.Quote.Type.NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
if (random.nextFloat() < 0.1 && quoteAble.isNotEmpty()) {
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.newBuilder().buildWith {
|
||||
id = random.nextLong(quoted.envelope.timestamp - 1000000, quoted.envelope.timestamp)
|
||||
quote = DataMessage.Quote.Builder().buildWith {
|
||||
id = random.nextLong(quoted.envelope.timestamp!! - 1000000, quoted.envelope.timestamp!!)
|
||||
authorAci = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage.body
|
||||
text = quoted.content.dataMessage?.body
|
||||
}
|
||||
}
|
||||
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val total = random.nextInt(1, 2)
|
||||
(0..total).forEach { _ -> addAttachments(attachmentPointer()) }
|
||||
attachments((0..total).map { attachmentPointer() })
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -166,12 +201,12 @@ object MessageContentFuzzer {
|
||||
* - A reaction to a prior message
|
||||
*/
|
||||
fun fuzzMediaMessageNoContent(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val reactTo = previousMessages.random(random)
|
||||
reaction = DataMessage.Reaction.newBuilder().buildWith {
|
||||
reaction = DataMessage.Reaction.Builder().buildWith {
|
||||
emoji = emojis.random(random)
|
||||
remove = false
|
||||
targetAuthorAci = reactTo.metadata.sourceServiceId.toString()
|
||||
@@ -183,22 +218,21 @@ object MessageContentFuzzer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that can never contain a text body. It may be:
|
||||
* - A sticker
|
||||
* Create a random media message that contains a sticker.
|
||||
*/
|
||||
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
if (random.nextFloat() < 0.9) {
|
||||
sticker = DataMessage.Sticker.newBuilder().buildWith {
|
||||
packId = byteString(length = 24)
|
||||
packKey = byteString(length = 128)
|
||||
stickerId = random.nextInt()
|
||||
data = attachmentPointer()
|
||||
emoji = emojis.random(random)
|
||||
}
|
||||
fun fuzzStickerMediaMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
|
||||
return Content.Builder()
|
||||
.dataMessage(
|
||||
DataMessage.Builder().buildWith {
|
||||
timestamp = sentTimestamp
|
||||
sticker = DataMessage.Sticker.Builder().buildWith {
|
||||
packId = byteString(length = 24)
|
||||
packKey = byteString(length = 128)
|
||||
stickerId = random.nextInt()
|
||||
data_ = attachmentPointer()
|
||||
emoji = emojis.random(random)
|
||||
}
|
||||
groupV2 = groupContextV2
|
||||
}
|
||||
).build()
|
||||
}
|
||||
@@ -223,14 +257,14 @@ object MessageContentFuzzer {
|
||||
* Generate a random [ByteString].
|
||||
*/
|
||||
fun byteString(length: Int = 512): ByteString {
|
||||
return random.nextBytes(length).toProtoByteString()
|
||||
return random.nextBytes(length).toByteString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random [AttachmentPointer].
|
||||
*/
|
||||
fun attachmentPointer(): AttachmentPointer {
|
||||
return AttachmentPointer.newBuilder().run {
|
||||
return AttachmentPointer.Builder().run {
|
||||
cdnKey = string()
|
||||
contentType = mediaTypes.random(random)
|
||||
key = byteString()
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.mockito.kotlin.anyOrNull
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.stub
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.signal.libsignal.protocol.util.KeyHelper
|
||||
import org.signal.libsignal.protocol.util.Medium
|
||||
import org.signal.libsignal.svr2.PinHash
|
||||
import org.thoughtcrime.securesms.crypto.PreKeyUtil
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.test.BuildConfig
|
||||
import org.whispersystems.signalservice.api.KeyBackupService
|
||||
import org.whispersystems.signalservice.api.SvrPinData
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
@@ -78,18 +68,6 @@ object MockProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fun mockGetRegistrationLockStringFlow() {
|
||||
val session: KeyBackupService.RestoreSession = object : KeyBackupService.RestoreSession {
|
||||
override fun hashSalt(): ByteArray = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8")
|
||||
override fun restorePin(hashedPin: PinHash?): SvrPinData = SvrPinData(MasterKey.createNew(SecureRandom()), null)
|
||||
}
|
||||
|
||||
val kbsService = ApplicationDependencies.getKeyBackupService(BuildConfig.KBS_ENCLAVE)
|
||||
kbsService.stub {
|
||||
on { newRegistrationSession(anyOrNull(), anyOrNull()) } doReturn session
|
||||
}
|
||||
}
|
||||
|
||||
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account().aciIdentityKey, deviceId: Int): PreKeyResponse {
|
||||
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
|
||||
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())
|
||||
|
||||
@@ -141,7 +141,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
|
||||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import org.signal.core.util.deleteAll
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.PNI
|
||||
@@ -34,7 +36,8 @@ class SignalDatabaseRule(
|
||||
|
||||
private fun deleteAllThreads() {
|
||||
if (deleteAllThreadsOnEachRun) {
|
||||
SignalDatabase.threads.clearForTests()
|
||||
SignalDatabase.threads.deleteAllConversations()
|
||||
SignalDatabase.rawDatabase.deleteAll(ThreadTable.TABLE_NAME)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.AddressProto
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto
|
||||
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
|
||||
import java.util.UUID
|
||||
import kotlin.random.Random
|
||||
|
||||
class TestProtos private constructor() {
|
||||
fun address(
|
||||
uuid: UUID = UUID.randomUUID()
|
||||
): AddressProto.Builder {
|
||||
return AddressProto.newBuilder()
|
||||
.setUuid(ACI.from(uuid).toByteString())
|
||||
}
|
||||
|
||||
fun metadata(
|
||||
address: AddressProto = address().build()
|
||||
): MetadataProto.Builder {
|
||||
return MetadataProto.newBuilder()
|
||||
.setAddress(address)
|
||||
}
|
||||
|
||||
fun groupContextV2(
|
||||
revision: Int = 0,
|
||||
masterKeyBytes: ByteArray = Random.Default.nextBytes(GroupMasterKey.SIZE)
|
||||
): GroupContextV2.Builder {
|
||||
return GroupContextV2.newBuilder()
|
||||
.setRevision(revision)
|
||||
.setMasterKey(ByteString.copyFrom(masterKeyBytes))
|
||||
}
|
||||
|
||||
fun storyContext(
|
||||
sentTimestamp: Long = Random.nextLong(),
|
||||
authorUuid: String = UUID.randomUUID().toString()
|
||||
): DataMessage.StoryContext.Builder {
|
||||
return DataMessage.StoryContext.newBuilder()
|
||||
.setAuthorAci(authorUuid)
|
||||
.setSentTimestamp(sentTimestamp)
|
||||
}
|
||||
|
||||
fun dataMessage(): DataMessage.Builder {
|
||||
return DataMessage.newBuilder()
|
||||
}
|
||||
|
||||
fun content(): SignalServiceProtos.Content.Builder {
|
||||
return SignalServiceProtos.Content.newBuilder()
|
||||
}
|
||||
|
||||
fun serviceContent(
|
||||
localAddress: AddressProto = address().build(),
|
||||
metadata: MetadataProto = metadata().build()
|
||||
): SignalServiceContentProto.Builder {
|
||||
return SignalServiceContentProto.newBuilder()
|
||||
.setLocalAddress(localAddress)
|
||||
.setMetadata(metadata)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <T> build(buildFn: TestProtos.() -> T): T {
|
||||
return TestProtos().buildFn()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,6 @@ package org.thoughtcrime.securesms.util;
|
||||
public final class FeatureFlagsAccessor {
|
||||
|
||||
public static void forceValue(String key, Object value) {
|
||||
FeatureFlags.FORCED_VALUES.put(FeatureFlags.PHONE_NUMBER_PRIVACY, true);
|
||||
FeatureFlags.FORCED_VALUES.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,8 @@ object MessageTableTestUtils {
|
||||
isKeyExchangeType:${type and MessageTypes.KEY_EXCHANGE_BIT != 0L}
|
||||
isIdentityVerified:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
|
||||
isIdentityDefault:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
|
||||
isCorruptedKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CORRUPTED_BIT != 0L}
|
||||
isInvalidVersionKeyExchange:${type and MessageTypes.KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
|
||||
isBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_BUNDLE_BIT != 0L}
|
||||
isContentBundleKeyExchange:${type and MessageTypes.KEY_EXCHANGE_CONTENT_FORMAT != 0L}
|
||||
isIdentityUpdate:${type and MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
|
||||
isRateLimited:${type and MessageTypes.MESSAGE_RATE_LIMITED_BIT != 0L}
|
||||
isExpirationTimerUpdate:${type and MessageTypes.EXPIRATION_TIMER_UPDATE_BIT != 0L}
|
||||
|
||||
@@ -2,9 +2,10 @@ package org.signal.benchmark.setup
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.MessageType
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.TestDbUtils
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -65,7 +66,8 @@ object TestMessages {
|
||||
return insert
|
||||
}
|
||||
fun insertIncomingTextMessage(other: Recipient, body: String, timestamp: Long? = null) {
|
||||
val message = IncomingMediaMessage(
|
||||
val message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
@@ -73,10 +75,11 @@ object TestMessages {
|
||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis()
|
||||
)
|
||||
|
||||
SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get().messageId
|
||||
SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get().messageId
|
||||
}
|
||||
fun insertIncomingQuoteTextMessage(other: Recipient, body: String, quote: QuoteModel, timestamp: Long?) {
|
||||
val message = IncomingMediaMessage(
|
||||
val message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = other.id,
|
||||
body = body,
|
||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
@@ -90,28 +93,30 @@ object TestMessages {
|
||||
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
|
||||
imageAttachment()
|
||||
}
|
||||
val message = IncomingMediaMessage(
|
||||
val message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = other.id,
|
||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
attachments = PointerAttachment.forPointers(Optional.of(attachments))
|
||||
)
|
||||
return insertIncomingMediaMessage(recipient = other, message = message, failed = failed)
|
||||
return insertIncomingMessage(recipient = other, message = message, failed = failed)
|
||||
}
|
||||
|
||||
fun insertIncomingVoiceMessage(other: Recipient, timestamp: Long? = null): Long {
|
||||
val message = IncomingMediaMessage(
|
||||
val message = IncomingMessage(
|
||||
type = MessageType.NORMAL,
|
||||
from = other.id,
|
||||
sentTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
serverTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
receivedTimeMillis = timestamp ?: System.currentTimeMillis(),
|
||||
attachments = PointerAttachment.forPointers(Optional.of(Collections.singletonList(voiceAttachment()) as List<SignalServiceAttachment>))
|
||||
)
|
||||
return insertIncomingMediaMessage(recipient = other, message = message, failed = false)
|
||||
return insertIncomingMessage(recipient = other, message = message, failed = false)
|
||||
}
|
||||
|
||||
private fun insertIncomingMediaMessage(recipient: Recipient, message: IncomingMediaMessage, failed: Boolean = false): Long {
|
||||
private fun insertIncomingMessage(recipient: Recipient, message: IncomingMessage, failed: Boolean = false): Long {
|
||||
val id = insertIncomingMessage(recipient = recipient, message = message)
|
||||
if (failed) {
|
||||
setMessageMediaFailed(id)
|
||||
@@ -122,8 +127,8 @@ object TestMessages {
|
||||
return id
|
||||
}
|
||||
|
||||
private fun insertIncomingMessage(recipient: Recipient, message: IncomingMediaMessage): Long {
|
||||
return SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(recipient)).get().messageId
|
||||
private fun insertIncomingMessage(recipient: Recipient, message: IncomingMessage): Long {
|
||||
return SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(recipient)).get().messageId
|
||||
}
|
||||
|
||||
private fun setMessageMediaFailed(messageId: Long) {
|
||||
@@ -149,6 +154,7 @@ object TestMessages {
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
0,
|
||||
Optional.of("/not-there.jpg"),
|
||||
false,
|
||||
false,
|
||||
@@ -171,6 +177,7 @@ object TestMessages {
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
0,
|
||||
Optional.of("/not-there.aac"),
|
||||
true,
|
||||
false,
|
||||
|
||||
@@ -100,7 +100,7 @@ object TestUsers {
|
||||
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
|
||||
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
|
||||
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
|
||||
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
|
||||
SignalDatabase.recipients.setProfileSharing(recipientId, true)
|
||||
SignalDatabase.recipients.markRegistered(recipientId, aci)
|
||||
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
@@ -78,7 +78,7 @@ class ConversationElementGenerator {
|
||||
|
||||
val isIncoming = random.nextBoolean()
|
||||
|
||||
val record = MediaMmsMessageRecord(
|
||||
val record = MmsMessageRecord(
|
||||
messageId,
|
||||
if (isIncoming) Recipient.UNKNOWN else Recipient.self(),
|
||||
0,
|
||||
@@ -86,7 +86,7 @@ class ConversationElementGenerator {
|
||||
now,
|
||||
now,
|
||||
now,
|
||||
1,
|
||||
true,
|
||||
1,
|
||||
testMessage,
|
||||
SlideDeck(),
|
||||
@@ -97,7 +97,7 @@ class ConversationElementGenerator {
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
1,
|
||||
true,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
@@ -106,7 +106,7 @@ class ConversationElementGenerator {
|
||||
false,
|
||||
false,
|
||||
now,
|
||||
1,
|
||||
true,
|
||||
now,
|
||||
null,
|
||||
StoryType.NONE,
|
||||
@@ -117,7 +117,9 @@ class ConversationElementGenerator {
|
||||
-1,
|
||||
null,
|
||||
null,
|
||||
0
|
||||
0,
|
||||
false,
|
||||
null
|
||||
)
|
||||
|
||||
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import com.bumptech.glide.Glide
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
@@ -33,6 +34,7 @@ import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
|
||||
import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
@@ -41,7 +43,6 @@ 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.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
@@ -61,11 +62,12 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = ConversationAdapterV2(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
glideRequests = GlideApp.with(this),
|
||||
requestManager = Glide.with(this),
|
||||
clickListener = ClickListener(),
|
||||
hasWallpaper = springboardViewModel.hasWallpaper.value,
|
||||
colorizer = Colorizer(),
|
||||
startExpirationTimeout = {}
|
||||
startExpirationTimeout = {},
|
||||
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) }
|
||||
)
|
||||
|
||||
if (springboardViewModel.hasWallpaper.value) {
|
||||
@@ -296,5 +298,17 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
|
||||
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onShowSafetyTips(forGroup: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onReportSpamLearnMoreClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onMessageRequestAcceptOptionsClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:replace="android:usesCleartextTraffic"
|
||||
|
||||
@@ -22,17 +22,10 @@
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
|
||||
<uses-permission android:name="android.permission.READ_PROFILE"/>
|
||||
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"
|
||||
tools:ignore="ProtectedPermissions"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
|
||||
<uses-permission android:name="android.permission.READ_SMS"/>
|
||||
<uses-permission android:name="android.permission.SEND_SMS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_SMS"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||
|
||||
@@ -50,16 +43,10 @@
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_STATE"/>
|
||||
|
||||
<!-- For sending/receiving events -->
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR"/>
|
||||
|
||||
|
||||
<!-- Normal -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
@@ -76,17 +63,14 @@
|
||||
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
|
||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||
|
||||
<!-- For fixing MMS -->
|
||||
<!-- For device transfer -->
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
|
||||
|
||||
<!-- Set image as wallpaper -->
|
||||
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
|
||||
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
|
||||
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
@@ -160,12 +144,6 @@
|
||||
android:value=".MainActivity" />
|
||||
</activity>
|
||||
|
||||
<activity android:name=".PromptMmsActivity"
|
||||
android:label="Configure MMS Settings"
|
||||
android:windowSoftInputMode="stateUnchanged"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".DeviceProvisioningActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="true">
|
||||
@@ -185,10 +163,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".preferences.MmsPreferencesActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false" />
|
||||
|
||||
<activity android:name=".sharing.interstitial.ShareInterstitialActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
@@ -252,6 +226,7 @@
|
||||
|
||||
<activity-alias android:name=".RoutingActivity"
|
||||
android:targetActivity=".MainActivity"
|
||||
android:resizeableActivity="true"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
@@ -593,6 +568,13 @@
|
||||
android:host="signal.group"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="signaldonations.org" android:pathPrefix="/stripe/return/ideal"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -630,6 +612,7 @@
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:resizeableActivity="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
@@ -684,7 +667,12 @@
|
||||
|
||||
<activity android:name=".NewConversationActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysVisible"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".recipients.ui.findby.FindByActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
@@ -716,6 +704,7 @@
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:exported="false"/>
|
||||
|
||||
@@ -759,6 +748,13 @@
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".backup.v2.ui.MessageBackupsFlowActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name=".stories.settings.StorySettingsActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
@@ -930,12 +926,12 @@
|
||||
</activity>
|
||||
|
||||
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:permission="android.permission.CALL_PHONE"
|
||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
android:exported="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:permission="android.permission.CALL_PHONE"
|
||||
android:theme="@style/NoAnimation.Theme.BlackScreen"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
@@ -961,17 +957,21 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".profiles.edit.EditProfileActivity"
|
||||
<activity android:name=".profiles.edit.CreateProfileActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".profiles.username.AddAUsernameActivity"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
<activity android:name=".backup.v2.ui.MessageBackupsTestRestoreActivity"
|
||||
android:theme="@style/TextSecure.LightRegistrationTheme"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".profiles.manage.EditProfileActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".profiles.manage.ManageProfileActivity"
|
||||
<activity android:name=".nicknames.NicknameActivity"
|
||||
android:theme="@style/TextSecure.LightTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"
|
||||
android:exported="false"/>
|
||||
@@ -997,7 +997,7 @@
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".contacts.TurnOffContactJoinedNotificationsActivity"
|
||||
android:theme="@style/Theme.AppCompat.Dialog.Alert"
|
||||
android:theme="@style/TextSecure.DialogActivity"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".contactshare.ContactShareEditActivity"
|
||||
@@ -1029,6 +1029,7 @@
|
||||
<activity android:name=".MainActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:resizeableActivity="true"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".pin.PinRestoreActivity"
|
||||
@@ -1062,13 +1063,6 @@
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".megaphone.SmsExportMegaphoneActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".ratelimit.RecaptchaProofActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||
@@ -1085,15 +1079,18 @@
|
||||
android:theme="@style/Theme.Signal.WallpaperCropper"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".reactions.edit.EditReactionsActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrImageSelectionActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/TextSecure.DarkNoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrScannerActivity"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity android:name=".exporter.flow.SmsExportActivity"
|
||||
<activity android:name=".reactions.edit.EditReactionsActivity"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
@@ -1102,12 +1099,6 @@
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:enabled="true"
|
||||
android:name=".exporter.SignalSmsExportService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:enabled="true"
|
||||
android:name=".service.webrtc.WebRtcCallService"
|
||||
@@ -1154,19 +1145,6 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name=".service.QuickResponseService"
|
||||
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
|
||||
android:exported="true" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".service.AccountAuthenticatorService" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
@@ -1186,6 +1164,10 @@
|
||||
android:name=".service.GenericForegroundService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".service.AttachmentProgressService"
|
||||
android:exported="false"/>
|
||||
|
||||
<service
|
||||
android:name=".gcm.FcmFetchBackgroundService"
|
||||
android:exported="false"/>
|
||||
@@ -1200,39 +1182,6 @@
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver android:name=".service.SmsListener"
|
||||
android:permission="android.permission.BROADCAST_SMS"
|
||||
android:enabled="true"
|
||||
android:exported="true">
|
||||
<intent-filter android:priority="1001">
|
||||
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.provider.Telephony.SMS_DELIVER"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.SmsDeliveryListener"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.services.MESSAGE_SENT"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".service.MmsListener"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BROADCAST_WAP_PUSH">
|
||||
<intent-filter android:priority="1001">
|
||||
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED"/>
|
||||
<data android:mimeType="application/vnd.wap.mms-message" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
|
||||
<data android:mimeType="application/vnd.wap.mms-message" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".notifications.MarkReadReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
@@ -1292,11 +1241,6 @@
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<provider android:name=".providers.MmsBodyProvider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false"
|
||||
android:authorities="${applicationId}.mms" />
|
||||
|
||||
<provider android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
@@ -1408,6 +1352,16 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService" android:exported="false" />
|
||||
<receiver android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallServiceReceiver" android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.DENY"/>
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.HANGUP"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
|
||||
|
||||
</application>
|
||||
|
||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 199 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 100 KiB |
BIN
app/src/main/assets/fonts/SignalSymbols-Bold.otf
Normal file
@@ -1,14 +1,13 @@
|
||||
package com.google.android.material.bottomsheet
|
||||
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
/**
|
||||
* Manually adjust the nested scrolling child for a given [BottomSheetBehavior].
|
||||
*/
|
||||
object BottomSheetBehaviorHack {
|
||||
fun setNestedScrollingChild(behavior: BottomSheetBehavior<FrameLayout>, view: View) {
|
||||
fun <T : View> setNestedScrollingChild(behavior: BottomSheetBehavior<T>, view: View) {
|
||||
behavior.nestedScrollingChildRef = WeakReference(view)
|
||||
}
|
||||
}
|
||||
|
||||
800
app/src/main/java/org/conscrypt/ConscryptSignal.java
Normal file
@@ -0,0 +1,800 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.conscrypt;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.cert.X509Certificate;
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLContextSpi;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLEngineResult;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLServerSocketFactory;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSessionContext;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
/**
|
||||
* Core API for creating and configuring all Conscrypt types.
|
||||
* This is identical to the original Conscrypt.java, except with the slow
|
||||
* version initialization code removed.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class ConscryptSignal {
|
||||
private ConscryptSignal() {}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the Conscrypt native library has been successfully loaded.
|
||||
*/
|
||||
public static boolean isAvailable() {
|
||||
try {
|
||||
checkAvailability();
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// BEGIN MODIFICATION
|
||||
/*public static class Version {
|
||||
private final int major;
|
||||
private final int minor;
|
||||
private final int patch;
|
||||
|
||||
private Version(int major, int minor, int patch) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.patch = patch;
|
||||
}
|
||||
|
||||
public int major() { return major; }
|
||||
public int minor() { return minor; }
|
||||
public int patch() { return patch; }
|
||||
}
|
||||
|
||||
private static final Version VERSION;
|
||||
|
||||
static {
|
||||
int major = -1;
|
||||
int minor = -1;
|
||||
int patch = -1;
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = Conscrypt.class.getResourceAsStream("conscrypt.properties");
|
||||
if (stream != null) {
|
||||
Properties props = new Properties();
|
||||
props.load(stream);
|
||||
major = Integer.parseInt(props.getProperty("org.conscrypt.version.major", "-1"));
|
||||
minor = Integer.parseInt(props.getProperty("org.conscrypt.version.minor", "-1"));
|
||||
patch = Integer.parseInt(props.getProperty("org.conscrypt.version.patch", "-1"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// TODO(prb): This should probably be fatal or have some fallback behaviour
|
||||
} finally {
|
||||
IoUtils.closeQuietly(stream);
|
||||
}
|
||||
if ((major >= 0) && (minor >= 0) && (patch >= 0)) {
|
||||
VERSION = new Version(major, minor, patch);
|
||||
} else {
|
||||
VERSION = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version of this distribution of Conscrypt. If version information is
|
||||
* unavailable, returns {@code null}.
|
||||
*/
|
||||
/*public static Version version() {
|
||||
return VERSION;
|
||||
}*/
|
||||
|
||||
// END MODIFICATION
|
||||
|
||||
/**
|
||||
* Checks that the Conscrypt support is available for the system.
|
||||
*
|
||||
* @throws UnsatisfiedLinkError if unavailable
|
||||
*/
|
||||
public static void checkAvailability() {
|
||||
NativeCrypto.checkAvailability();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the given {@link Provider} was created by this distribution of Conscrypt.
|
||||
*/
|
||||
public static boolean isConscrypt(Provider provider) {
|
||||
return provider instanceof OpenSSLProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Provider} with the default name.
|
||||
*/
|
||||
public static Provider newProvider() {
|
||||
checkAvailability();
|
||||
return new OpenSSLProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Provider} with the given name.
|
||||
*
|
||||
* @deprecated Use {@link #newProviderBuilder()} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Provider newProvider(String providerName) {
|
||||
checkAvailability();
|
||||
return newProviderBuilder().setName(providerName).build();
|
||||
}
|
||||
|
||||
public static class ProviderBuilder {
|
||||
private String name = Platform.getDefaultProviderName();
|
||||
private boolean provideTrustManager = Platform.provideTrustManagerByDefault();
|
||||
private String defaultTlsProtocol = NativeCrypto.SUPPORTED_PROTOCOL_TLSV1_3;
|
||||
|
||||
private ProviderBuilder() {}
|
||||
|
||||
/**
|
||||
* Sets the name of the Provider to be built.
|
||||
*/
|
||||
public ProviderBuilder setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes the returned provider to provide an implementation of
|
||||
* {@link javax.net.ssl.TrustManagerFactory}.
|
||||
* @deprecated Use provideTrustManager(true)
|
||||
*/
|
||||
@Deprecated
|
||||
public ProviderBuilder provideTrustManager() {
|
||||
return provideTrustManager(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies whether the returned provider will provide an implementation of
|
||||
* {@link javax.net.ssl.TrustManagerFactory}.
|
||||
*/
|
||||
public ProviderBuilder provideTrustManager(boolean provide) {
|
||||
this.provideTrustManager = provide;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies what the default TLS protocol should be for SSLContext identifiers
|
||||
* {@code TLS}, {@code SSL}, and {@code Default}.
|
||||
*/
|
||||
public ProviderBuilder defaultTlsProtocol(String defaultTlsProtocol) {
|
||||
this.defaultTlsProtocol = defaultTlsProtocol;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Provider build() {
|
||||
return new OpenSSLProvider(name, provideTrustManager, defaultTlsProtocol);
|
||||
}
|
||||
}
|
||||
|
||||
public static ProviderBuilder newProviderBuilder() {
|
||||
return new ProviderBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum length (in bytes) of an encrypted packet.
|
||||
*/
|
||||
public static int maxEncryptedPacketLength() {
|
||||
return NativeConstants.SSL3_RT_MAX_PACKET_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default X.509 trust manager.
|
||||
*/
|
||||
@ExperimentalApi
|
||||
public static X509TrustManager getDefaultX509TrustManager() throws KeyManagementException {
|
||||
checkAvailability();
|
||||
return SSLParametersImpl.getDefaultX509TrustManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the given {@link SSLContext} was created by this distribution of Conscrypt.
|
||||
*/
|
||||
public static boolean isConscrypt(SSLContext context) {
|
||||
return context.getProvider() instanceof OpenSSLProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance of the preferred {@link SSLContextSpi}.
|
||||
*/
|
||||
public static SSLContextSpi newPreferredSSLContextSpi() {
|
||||
checkAvailability();
|
||||
return OpenSSLContextImpl.getPreferred();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client-side persistent cache to be used by the context.
|
||||
*/
|
||||
public static void setClientSessionCache(SSLContext context, SSLClientSessionCache cache) {
|
||||
SSLSessionContext clientContext = context.getClientSessionContext();
|
||||
if (!(clientContext instanceof ClientSessionContext)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not a conscrypt client context: " + clientContext.getClass().getName());
|
||||
}
|
||||
((ClientSessionContext) clientContext).setPersistentCache(cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the server-side persistent cache to be used by the context.
|
||||
*/
|
||||
public static void setServerSessionCache(SSLContext context, SSLServerSessionCache cache) {
|
||||
SSLSessionContext serverContext = context.getServerSessionContext();
|
||||
if (!(serverContext instanceof ServerSessionContext)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not a conscrypt client context: " + serverContext.getClass().getName());
|
||||
}
|
||||
((ServerSessionContext) serverContext).setPersistentCache(cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the given {@link SSLSocketFactory} was created by this distribution of
|
||||
* Conscrypt.
|
||||
*/
|
||||
public static boolean isConscrypt(SSLSocketFactory factory) {
|
||||
return factory instanceof OpenSSLSocketFactoryImpl;
|
||||
}
|
||||
|
||||
private static OpenSSLSocketFactoryImpl toConscrypt(SSLSocketFactory factory) {
|
||||
if (!isConscrypt(factory)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not a conscrypt socket factory: " + factory.getClass().getName());
|
||||
}
|
||||
return (OpenSSLSocketFactoryImpl) factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the default socket to be created for all socket factory instances.
|
||||
*/
|
||||
@ExperimentalApi
|
||||
public static void setUseEngineSocketByDefault(boolean useEngineSocket) {
|
||||
OpenSSLSocketFactoryImpl.setUseEngineSocketByDefault(useEngineSocket);
|
||||
OpenSSLServerSocketFactoryImpl.setUseEngineSocketByDefault(useEngineSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the socket to be created for the given socket factory instance.
|
||||
*/
|
||||
@ExperimentalApi
|
||||
public static void setUseEngineSocket(SSLSocketFactory factory, boolean useEngineSocket) {
|
||||
toConscrypt(factory).setUseEngineSocket(useEngineSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the given {@link SSLServerSocketFactory} was created by this distribution
|
||||
* of Conscrypt.
|
||||
*/
|
||||
public static boolean isConscrypt(SSLServerSocketFactory factory) {
|
||||
return factory instanceof OpenSSLServerSocketFactoryImpl;
|
||||
}
|
||||
|
||||
private static OpenSSLServerSocketFactoryImpl toConscrypt(SSLServerSocketFactory factory) {
|
||||
if (!isConscrypt(factory)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not a conscrypt server socket factory: " + factory.getClass().getName());
|
||||
}
|
||||
return (OpenSSLServerSocketFactoryImpl) factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the socket to be created for the given server socket factory instance.
|
||||
*/
|
||||
@ExperimentalApi
|
||||
public static void setUseEngineSocket(SSLServerSocketFactory factory, boolean useEngineSocket) {
|
||||
toConscrypt(factory).setUseEngineSocket(useEngineSocket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the given {@link SSLSocket} was created by this distribution of Conscrypt.
|
||||
*/
|
||||
public static boolean isConscrypt(SSLSocket socket) {
|
||||
return socket instanceof AbstractConscryptSocket;
|
||||
}
|
||||
|
||||
private static AbstractConscryptSocket toConscrypt(SSLSocket socket) {
|
||||
if (!isConscrypt(socket)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not a conscrypt socket: " + socket.getClass().getName());
|
||||
}
|
||||
return (AbstractConscryptSocket) socket;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method enables Server Name Indication (SNI) and overrides the hostname supplied
|
||||
* during socket creation. If the hostname is not a valid SNI hostname, the SNI extension
|
||||
* will be omitted from the handshake.
|
||||
*
|
||||
* @param socket the socket
|
||||
* @param hostname the desired SNI hostname, or null to disable
|
||||
*/
|
||||
public static void setHostname(SSLSocket socket, String hostname) {
|
||||
toConscrypt(socket).setHostname(hostname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either the hostname supplied during socket creation or via
|
||||
* {@link #setHostname(SSLSocket, String)}. No DNS resolution is attempted before
|
||||
* returning the hostname.
|
||||
*/
|
||||
public static String getHostname(SSLSocket socket) {
|
||||
return toConscrypt(socket).getHostname();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method attempts to create a textual representation of the peer host or IP. Does
|
||||
* not perform a reverse DNS lookup. This is typically used during session creation.
|
||||
*/
|
||||
public static String getHostnameOrIP(SSLSocket socket) {
|
||||
return toConscrypt(socket).getHostnameOrIP();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method enables session ticket support.
|
||||
*
|
||||
* @param socket the socket
|
||||
* @param useSessionTickets True to enable session tickets
|
||||
*/
|
||||
public static void setUseSessionTickets(SSLSocket socket, boolean useSessionTickets) {
|
||||
toConscrypt(socket).setUseSessionTickets(useSessionTickets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables/disables TLS Channel ID for the given server-side socket.
|
||||
*
|
||||
* <p>This method needs to be invoked before the handshake starts.
|
||||
*
|
||||
* @param socket the socket
|
||||
* @param enabled Whether to enable channel ID.
|
||||
* @throws IllegalStateException if this is a client socket or if the handshake has already
|
||||
* started.
|
||||
*/
|
||||
public static void setChannelIdEnabled(SSLSocket socket, boolean enabled) {
|
||||
toConscrypt(socket).setChannelIdEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the TLS Channel ID for the given server-side socket. Channel ID is only available
|
||||
* once the handshake completes.
|
||||
*
|
||||
* @param socket the socket
|
||||
* @return channel ID or {@code null} if not available.
|
||||
* @throws IllegalStateException if this is a client socket or if the handshake has not yet
|
||||
* completed.
|
||||
* @throws SSLException if channel ID is available but could not be obtained.
|
||||
*/
|
||||
public static byte[] getChannelId(SSLSocket socket) throws SSLException {
|
||||
return toConscrypt(socket).getChannelId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link PrivateKey} to be used for TLS Channel ID by this client socket.
|
||||
*
|
||||
* <p>This method needs to be invoked before the handshake starts.
|
||||
*
|
||||
* @param socket the socket
|
||||
* @param privateKey private key (enables TLS Channel ID) or {@code null} for no key
|
||||
* (disables TLS Channel ID).
|
||||
* The private key must be an Elliptic Curve (EC) key based on the NIST P-256 curve (aka
|
||||
* SECG secp256r1 or ANSI
|
||||
* X9.62 prime256v1).
|
||||
* @throws IllegalStateException if this is a server socket or if the handshake has already
|
||||
* started.
|
||||
*/
|
||||
public static void setChannelIdPrivateKey(SSLSocket socket, PrivateKey privateKey) {
|
||||
toConscrypt(socket).setChannelIdPrivateKey(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ALPN protocol agreed upon by client and server.
|
||||
*
|
||||
* @param socket the socket
|
||||
* @return the selected protocol or {@code null} if no protocol was agreed upon.
|
||||
*/
|
||||
public static String getApplicationProtocol(SSLSocket socket) {
|
||||
return toConscrypt(socket).getApplicationProtocol();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an application-provided ALPN protocol selector. If provided, this will override
|
||||
* the list of protocols set by {@link #setApplicationProtocols(SSLSocket, String[])}.
|
||||
*
|
||||
* @param socket the socket
|
||||
* @param selector the ALPN protocol selector
|
||||
*/
|
||||
public static void setApplicationProtocolSelector(SSLSocket socket,
|
||||
ApplicationProtocolSelector selector) {
|
||||
toConscrypt(socket).setApplicationProtocolSelector(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the application-layer protocols (ALPN) in prioritization order.
|
||||
*
|
||||
* @param socket the socket being configured
|
||||
* @param protocols the protocols in descending order of preference. If empty, no protocol
|
||||
* indications will be used. This array will be copied.
|
||||
* @throws IllegalArgumentException - if protocols is null, or if any element in a non-empty
|
||||
* array is null or an empty (zero-length) string
|
||||
*/
|
||||
public static void setApplicationProtocols(SSLSocket socket, String[] protocols) {
|
||||
toConscrypt(socket).setApplicationProtocols(protocols);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application-layer protocols (ALPN) in prioritization order.
|
||||
*
|
||||
* @param socket the socket
|
||||
* @return the protocols in descending order of preference, or an empty array if protocol
|
||||
* indications are not being used. Always returns a new array.
|
||||
*/
|
||||
public static String[] getApplicationProtocols(SSLSocket socket) {
|
||||
return toConscrypt(socket).getApplicationProtocols();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tls-unique channel binding value for this connection, per RFC 5929. This
|
||||
* will return {@code null} if there is no such value available, such as if the handshake
|
||||
* has not yet completed or this connection is closed.
|
||||
*/
|
||||
public static byte[] getTlsUnique(SSLSocket socket) {
|
||||
return toConscrypt(socket).getTlsUnique();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a value derived from the TLS master secret as described in RFC 5705.
|
||||
*
|
||||
* @param label the label to use in calculating the exported value. This must be
|
||||
* an ASCII-only string.
|
||||
* @param context the application-specific context value to use in calculating the
|
||||
* exported value. This may be {@code null} to use no application context, which is
|
||||
* treated differently than an empty byte array.
|
||||
* @param length the number of bytes of keying material to return.
|
||||
* @return a value of the specified length, or {@code null} if the handshake has not yet
|
||||
* completed or the connection has been closed.
|
||||
* @throws SSLException if the value could not be exported.
|
||||
*/
|
||||
public static byte[] exportKeyingMaterial(SSLSocket socket, String label, byte[] context,
|
||||
int length) throws SSLException {
|
||||
return toConscrypt(socket).exportKeyingMaterial(label, context, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the given {@link SSLEngine} was created by this distribution of Conscrypt.
|
||||
*/
|
||||
public static boolean isConscrypt(SSLEngine engine) {
|
||||
return engine instanceof AbstractConscryptEngine;
|
||||
}
|
||||
|
||||
private static AbstractConscryptEngine toConscrypt(SSLEngine engine) {
|
||||
if (!isConscrypt(engine)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not a conscrypt engine: " + engine.getClass().getName());
|
||||
}
|
||||
return (AbstractConscryptEngine) engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the given engine with the provided bufferAllocator.
|
||||
* @throws IllegalArgumentException if the provided engine is not a Conscrypt engine.
|
||||
* @throws IllegalStateException if the provided engine has already begun its handshake.
|
||||
*/
|
||||
@ExperimentalApi
|
||||
public static void setBufferAllocator(SSLEngine engine, BufferAllocator bufferAllocator) {
|
||||
toConscrypt(engine).setBufferAllocator(bufferAllocator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the given socket with the provided bufferAllocator. If the given socket is a
|
||||
* Conscrypt socket but does not use buffer allocators, this method does nothing.
|
||||
* @throws IllegalArgumentException if the provided socket is not a Conscrypt socket.
|
||||
* @throws IllegalStateException if the provided socket has already begun its handshake.
|
||||
*/
|
||||
@ExperimentalApi
|
||||
public static void setBufferAllocator(SSLSocket socket, BufferAllocator bufferAllocator) {
|
||||
AbstractConscryptSocket s = toConscrypt(socket);
|
||||
if (s instanceof ConscryptEngineSocket) {
|
||||
((ConscryptEngineSocket) s).setBufferAllocator(bufferAllocator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the default {@link BufferAllocator} to be used by all future
|
||||
* {@link SSLEngine} instances from this provider.
|
||||
*/
|
||||
@ExperimentalApi
|
||||
public static void setDefaultBufferAllocator(BufferAllocator bufferAllocator) {
|
||||
ConscryptEngine.setDefaultBufferAllocator(bufferAllocator);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method enables Server Name Indication (SNI) and overrides the hostname supplied
|
||||
* during engine creation.
|
||||
*
|
||||
* @param engine the engine
|
||||
* @param hostname the desired SNI hostname, or {@code null} to disable
|
||||
*/
|
||||
public static void setHostname(SSLEngine engine, String hostname) {
|
||||
toConscrypt(engine).setHostname(hostname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either the hostname supplied during socket creation or via
|
||||
* {@link #setHostname(SSLEngine, String)}. No DNS resolution is attempted before
|
||||
* returning the hostname.
|
||||
*/
|
||||
public static String getHostname(SSLEngine engine) {
|
||||
return toConscrypt(engine).getHostname();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum overhead, in bytes, of sealing a record with SSL.
|
||||
*/
|
||||
public static int maxSealOverhead(SSLEngine engine) {
|
||||
return toConscrypt(engine).maxSealOverhead();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a listener on the given engine for completion of the TLS handshake
|
||||
*/
|
||||
public static void setHandshakeListener(SSLEngine engine, HandshakeListener handshakeListener) {
|
||||
toConscrypt(engine).setHandshakeListener(handshakeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables/disables TLS Channel ID for the given server-side engine.
|
||||
*
|
||||
* <p>This method needs to be invoked before the handshake starts.
|
||||
*
|
||||
* @param engine the engine
|
||||
* @param enabled Whether to enable channel ID.
|
||||
* @throws IllegalStateException if this is a client engine or if the handshake has already
|
||||
* started.
|
||||
*/
|
||||
public static void setChannelIdEnabled(SSLEngine engine, boolean enabled) {
|
||||
toConscrypt(engine).setChannelIdEnabled(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the TLS Channel ID for the given server-side engine. Channel ID is only available
|
||||
* once the handshake completes.
|
||||
*
|
||||
* @param engine the engine
|
||||
* @return channel ID or {@code null} if not available.
|
||||
* @throws IllegalStateException if this is a client engine or if the handshake has not yet
|
||||
* completed.
|
||||
* @throws SSLException if channel ID is available but could not be obtained.
|
||||
*/
|
||||
public static byte[] getChannelId(SSLEngine engine) throws SSLException {
|
||||
return toConscrypt(engine).getChannelId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link PrivateKey} to be used for TLS Channel ID by this client engine.
|
||||
*
|
||||
* <p>This method needs to be invoked before the handshake starts.
|
||||
*
|
||||
* @param engine the engine
|
||||
* @param privateKey private key (enables TLS Channel ID) or {@code null} for no key
|
||||
* (disables TLS Channel ID).
|
||||
* The private key must be an Elliptic Curve (EC) key based on the NIST P-256 curve (aka
|
||||
* SECG secp256r1 or ANSI X9.62 prime256v1).
|
||||
* @throws IllegalStateException if this is a server engine or if the handshake has already
|
||||
* started.
|
||||
*/
|
||||
public static void setChannelIdPrivateKey(SSLEngine engine, PrivateKey privateKey) {
|
||||
toConscrypt(engine).setChannelIdPrivateKey(privateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended unwrap method for multiple source and destination buffers.
|
||||
*
|
||||
* @param engine the target engine for the unwrap
|
||||
* @param srcs the source buffers
|
||||
* @param dsts the destination buffers
|
||||
* @return the result of the unwrap operation
|
||||
* @throws SSLException thrown if an SSL error occurred
|
||||
*/
|
||||
public static SSLEngineResult unwrap(SSLEngine engine, final ByteBuffer[] srcs,
|
||||
final ByteBuffer[] dsts) throws SSLException {
|
||||
return toConscrypt(engine).unwrap(srcs, dsts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exteneded unwrap method for multiple source and destination buffers.
|
||||
*
|
||||
* @param engine the target engine for the unwrap.
|
||||
* @param srcs the source buffers
|
||||
* @param srcsOffset the offset in the {@code srcs} array of the first source buffer
|
||||
* @param srcsLength the number of source buffers starting at {@code srcsOffset}
|
||||
* @param dsts the destination buffers
|
||||
* @param dstsOffset the offset in the {@code dsts} array of the first destination buffer
|
||||
* @param dstsLength the number of destination buffers starting at {@code dstsOffset}
|
||||
* @return the result of the unwrap operation
|
||||
* @throws SSLException thrown if an SSL error occurred
|
||||
*/
|
||||
public static SSLEngineResult unwrap(SSLEngine engine, final ByteBuffer[] srcs, int srcsOffset,
|
||||
final int srcsLength, final ByteBuffer[] dsts, final int dstsOffset,
|
||||
final int dstsLength) throws SSLException {
|
||||
return toConscrypt(engine).unwrap(
|
||||
srcs, srcsOffset, srcsLength, dsts, dstsOffset, dstsLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method enables session ticket support.
|
||||
*
|
||||
* @param engine the engine
|
||||
* @param useSessionTickets True to enable session tickets
|
||||
*/
|
||||
public static void setUseSessionTickets(SSLEngine engine, boolean useSessionTickets) {
|
||||
toConscrypt(engine).setUseSessionTickets(useSessionTickets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the application-layer protocols (ALPN) in prioritization order.
|
||||
*
|
||||
* @param engine the engine being configured
|
||||
* @param protocols the protocols in descending order of preference. If empty, no protocol
|
||||
* indications will be used. This array will be copied.
|
||||
* @throws IllegalArgumentException - if protocols is null, or if any element in a non-empty
|
||||
* array is null or an empty (zero-length) string
|
||||
*/
|
||||
public static void setApplicationProtocols(SSLEngine engine, String[] protocols) {
|
||||
toConscrypt(engine).setApplicationProtocols(protocols);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application-layer protocols (ALPN) in prioritization order.
|
||||
*
|
||||
* @param engine the engine
|
||||
* @return the protocols in descending order of preference, or an empty array if protocol
|
||||
* indications are not being used. Always returns a new array.
|
||||
*/
|
||||
public static String[] getApplicationProtocols(SSLEngine engine) {
|
||||
return toConscrypt(engine).getApplicationProtocols();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an application-provided ALPN protocol selector. If provided, this will override
|
||||
* the list of protocols set by {@link #setApplicationProtocols(SSLEngine, String[])}.
|
||||
*
|
||||
* @param engine the engine
|
||||
* @param selector the ALPN protocol selector
|
||||
*/
|
||||
public static void setApplicationProtocolSelector(SSLEngine engine,
|
||||
ApplicationProtocolSelector selector) {
|
||||
toConscrypt(engine).setApplicationProtocolSelector(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ALPN protocol agreed upon by client and server.
|
||||
*
|
||||
* @param engine the engine
|
||||
* @return the selected protocol or {@code null} if no protocol was agreed upon.
|
||||
*/
|
||||
public static String getApplicationProtocol(SSLEngine engine) {
|
||||
return toConscrypt(engine).getApplicationProtocol();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tls-unique channel binding value for this connection, per RFC 5929. This
|
||||
* will return {@code null} if there is no such value available, such as if the handshake
|
||||
* has not yet completed or this connection is closed.
|
||||
*/
|
||||
public static byte[] getTlsUnique(SSLEngine engine) {
|
||||
return toConscrypt(engine).getTlsUnique();
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a value derived from the TLS master secret as described in RFC 5705.
|
||||
*
|
||||
* @param label the label to use in calculating the exported value. This must be
|
||||
* an ASCII-only string.
|
||||
* @param context the application-specific context value to use in calculating the
|
||||
* exported value. This may be {@code null} to use no application context, which is
|
||||
* treated differently than an empty byte array.
|
||||
* @param length the number of bytes of keying material to return.
|
||||
* @return a value of the specified length, or {@code null} if the handshake has not yet
|
||||
* completed or the connection has been closed.
|
||||
* @throws SSLException if the value could not be exported.
|
||||
*/
|
||||
public static byte[] exportKeyingMaterial(SSLEngine engine, String label, byte[] context,
|
||||
int length) throws SSLException {
|
||||
return toConscrypt(engine).exportKeyingMaterial(label, context, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the given {@link TrustManager} was created by this distribution of
|
||||
* Conscrypt.
|
||||
*/
|
||||
public static boolean isConscrypt(TrustManager trustManager) {
|
||||
return trustManager instanceof TrustManagerImpl;
|
||||
}
|
||||
|
||||
private static TrustManagerImpl toConscrypt(TrustManager trustManager) {
|
||||
if (!isConscrypt(trustManager)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not a Conscrypt trust manager: " + trustManager.getClass().getName());
|
||||
}
|
||||
return (TrustManagerImpl) trustManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default hostname verifier that will be used for HTTPS endpoint identification by
|
||||
* Conscrypt trust managers. If {@code null} (the default), endpoint identification will use
|
||||
* the default hostname verifier set in
|
||||
* {@link HttpsURLConnection#setDefaultHostnameVerifier(javax.net.ssl.HostnameVerifier)}.
|
||||
*/
|
||||
public synchronized static void setDefaultHostnameVerifier(ConscryptHostnameVerifier verifier) {
|
||||
TrustManagerImpl.setDefaultHostnameVerifier(verifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently-set default hostname verifier for Conscrypt trust managers.
|
||||
*
|
||||
* @see #setDefaultHostnameVerifier(ConscryptHostnameVerifier)
|
||||
*/
|
||||
public synchronized static ConscryptHostnameVerifier getDefaultHostnameVerifier(TrustManager trustManager) {
|
||||
return TrustManagerImpl.getDefaultHostnameVerifier();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hostname verifier that will be used for HTTPS endpoint identification by the
|
||||
* given trust manager. If {@code null} (the default), endpoint identification will use the
|
||||
* default hostname verifier set in {@link #setDefaultHostnameVerifier(ConscryptHostnameVerifier)}.
|
||||
*
|
||||
* @throws IllegalArgumentException if the provided trust manager is not a Conscrypt trust
|
||||
* manager per {@link #isConscrypt(TrustManager)}
|
||||
*/
|
||||
public static void setHostnameVerifier(TrustManager trustManager, ConscryptHostnameVerifier verifier) {
|
||||
toConscrypt(trustManager).setHostnameVerifier(verifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently-set hostname verifier for the given trust manager.
|
||||
*
|
||||
* @throws IllegalArgumentException if the provided trust manager is not a Conscrypt trust
|
||||
* manager per {@link #isConscrypt(TrustManager)}
|
||||
*
|
||||
* @see #setHostnameVerifier(TrustManager, ConscryptHostnameVerifier)
|
||||
*/
|
||||
public static ConscryptHostnameVerifier getHostnameVerifier(TrustManager trustManager) {
|
||||
return toConscrypt(trustManager).getHostnameVerifier();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the HttpsURLConnection.HostnameVerifier into a ConscryptHostnameVerifier
|
||||
*/
|
||||
public static ConscryptHostnameVerifier wrapHostnameVerifier(final HostnameVerifier verifier) {
|
||||
return new ConscryptHostnameVerifier() {
|
||||
@Override
|
||||
public boolean verify(X509Certificate[] certificates, String hostname, SSLSession session) {
|
||||
return verifier.verify(hostname, session);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.signalservice.api.account.AccountAttributes
|
||||
|
||||
object AppCapabilities {
|
||||
@@ -17,7 +16,7 @@ object AppCapabilities {
|
||||
changeNumber = true,
|
||||
stories = true,
|
||||
giftBadges = true,
|
||||
pni = FeatureFlags.phoneNumberPrivacy(),
|
||||
pni = true,
|
||||
paymentActivation = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,15 +24,18 @@ import androidx.annotation.VisibleForTesting;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.conscrypt.ConscryptSignal;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.core.util.MemoryTracker;
|
||||
import org.signal.core.util.concurrent.AnrDetector;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.AndroidLogger;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.core.util.logging.Scrubber;
|
||||
import org.signal.core.util.tracing.Tracer;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
@@ -52,9 +55,12 @@ import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
|
||||
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.ExternalLaunchDonationJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
|
||||
@@ -70,7 +76,6 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
|
||||
import org.thoughtcrime.securesms.mms.SignalGlideModule;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
@@ -83,15 +88,13 @@ import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.service.LocalBackupListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
|
||||
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
|
||||
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
|
||||
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.util.AppForegroundObserver;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.PowerManagerCompat;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -110,6 +113,7 @@ import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
import kotlin.Unit;
|
||||
import rxdogtag2.RxDogTag;
|
||||
|
||||
/**
|
||||
@@ -124,9 +128,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
private static final String TAG = Log.tag(ApplicationContext.class);
|
||||
|
||||
@VisibleForTesting
|
||||
protected PersistentLogger persistentLogger;
|
||||
|
||||
public static ApplicationContext getInstance(Context context) {
|
||||
return (ApplicationContext)context.getApplicationContext();
|
||||
}
|
||||
@@ -155,28 +156,30 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
})
|
||||
.addBlocking("anr-detector", this::startAnrDetector)
|
||||
.addBlocking("security-provider", this::initializeSecurityProvider)
|
||||
.addBlocking("crash-handling", this::initializeCrashHandling)
|
||||
.addBlocking("rx-init", this::initializeRx)
|
||||
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
|
||||
.addBlocking("app-dependencies", this::initializeAppDependencies)
|
||||
.addBlocking("scrubber", () -> Scrubber.setIdentifierHmacKeyProvider(() -> SignalStore.svr().getOrCreateMasterKey().deriveLoggingKey()))
|
||||
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
|
||||
.addBlocking("app-migrations", this::initializeApplicationMigrations)
|
||||
.addBlocking("mark-registration", () -> RegistrationUtil.maybeMarkRegistrationComplete())
|
||||
.addBlocking("lifecycle-observer", () -> ApplicationDependencies.getAppForegroundObserver().addListener(this))
|
||||
.addBlocking("message-retriever", this::initializeMessageRetrieval)
|
||||
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
|
||||
.addBlocking("proxy-init", () -> {
|
||||
if (SignalStore.proxy().isProxyEnabled()) {
|
||||
Log.w(TAG, "Proxy detected. Enabling Conscrypt.setUseEngineSocketByDefault()");
|
||||
Conscrypt.setUseEngineSocketByDefault(true);
|
||||
ConscryptSignal.setUseEngineSocketByDefault(true);
|
||||
}
|
||||
})
|
||||
.addBlocking("blob-provider", this::initializeBlobProvider)
|
||||
.addBlocking("feature-flags", FeatureFlags::init)
|
||||
.addBlocking("ring-rtc", this::initializeRingRtc)
|
||||
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
|
||||
.addNonBlocking(() -> GlideApp.get(this))
|
||||
.addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete())
|
||||
.addNonBlocking(() -> Glide.get(this))
|
||||
.addNonBlocking(this::cleanAvatarStorage)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
@@ -188,7 +191,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(this::initializeCleanup)
|
||||
.addNonBlocking(this::initializeGlideCodecs)
|
||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
|
||||
.addNonBlocking(this::beginJobLoop)
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addNonBlocking(this::ensureProfileUploaded)
|
||||
@@ -196,7 +199,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(this::initializeTrimThreadsByDateManager)
|
||||
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
@@ -213,6 +215,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
|
||||
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
|
||||
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
|
||||
.addPostRender(GroupRingCleanupJob::enqueue)
|
||||
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -229,11 +233,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
ApplicationDependencies.getDeadlockDetector().start();
|
||||
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
|
||||
ExternalLaunchDonationJob.enqueueIfNecessary();
|
||||
FcmFetchManager.onForeground(this);
|
||||
startAnrDetector();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
|
||||
RetrieveProfileJob.enqueueRoutineFetchIfNecessary();
|
||||
executePendingContactSync();
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getShakeToReport().enable();
|
||||
@@ -263,19 +269,27 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getShakeToReport().disable();
|
||||
ApplicationDependencies.getDeadlockDetector().stop();
|
||||
MemoryTracker.stop();
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
return persistentLogger;
|
||||
AnrDetector.stop();
|
||||
}
|
||||
|
||||
public void checkBuildExpiration() {
|
||||
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
|
||||
Log.w(TAG, "Build expired!");
|
||||
SignalStore.misc().markClientDeprecated();
|
||||
SignalStore.misc().setClientDeprecated(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this is purposefully "started" twice -- once during application create, and once during foreground.
|
||||
* This is so we can capture ANR's that happen on boot before the foreground event.
|
||||
*/
|
||||
private void startAnrDetector() {
|
||||
AnrDetector.start(TimeUnit.SECONDS.toMillis(5), FeatureFlags::internalUser, (dumps) -> {
|
||||
LogDatabase.getInstance(this).anrs().save(System.currentTimeMillis(), dumps);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeSecurityProvider() {
|
||||
int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1);
|
||||
Log.i(TAG, "Installed AesGcmProvider: " + aesPosition);
|
||||
@@ -285,7 +299,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
throw new ProviderInitializationException();
|
||||
}
|
||||
|
||||
int conscryptPosition = Security.insertProviderAt(Conscrypt.newProvider(), 2);
|
||||
int conscryptPosition = Security.insertProviderAt(ConscryptSignal.newProvider(), 2);
|
||||
Log.i(TAG, "Installed Conscrypt provider: " + conscryptPosition);
|
||||
|
||||
if (conscryptPosition < 0) {
|
||||
@@ -295,14 +309,14 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
@VisibleForTesting
|
||||
protected void initializeLogging() {
|
||||
persistentLogger = new PersistentLogger(this);
|
||||
org.signal.core.util.logging.Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), persistentLogger);
|
||||
Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), new PersistentLogger(this));
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
||||
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
Log.blockUntilAllWritesFinished();
|
||||
LogDatabase.getInstance(this).trimToSize();
|
||||
LogDatabase.getInstance(this).logs().trimToSize();
|
||||
LogDatabase.getInstance(this).crashes().trimToSize();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -406,8 +420,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
RotateSenderCertificateListener.schedule(this);
|
||||
RoutineMessageFetchReceiver.startOrUpdateAlarm(this);
|
||||
|
||||
if (BuildConfig.PLAY_STORE_DISABLED) {
|
||||
UpdateApkRefreshListener.schedule(this);
|
||||
if (BuildConfig.MANAGES_APP_UPDATES) {
|
||||
ApkUpdateRefreshListener.schedule(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,6 +431,9 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
if (FeatureFlags.callingFieldTrialAnyAddressPortsKillSwitch()) {
|
||||
fieldTrials.put("RingRTC-AnyAddressPortsKillSwitch", "Enabled");
|
||||
}
|
||||
if (!SignalStore.internalValues().callingDisableLBRed()) {
|
||||
fieldTrials.put("RingRTC-Audio-LBRed-For-Opus", "Enabled,bitrate_pri:22000");
|
||||
}
|
||||
CallManager.initialize(this, new RingRtcLogger(), fieldTrials);
|
||||
} catch (UnsatisfiedLinkError e) {
|
||||
throw new AssertionError("Unable to load ringrtc library", e);
|
||||
@@ -447,6 +464,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void beginJobLoop() {
|
||||
ApplicationDependencies.getJobManager().beginJobLoop();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void initializeBlobProvider() {
|
||||
BlobProvider.getInstance().initialize(this);
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.transition.TransitionInflater;
|
||||
import android.view.View;
|
||||
@@ -19,6 +18,7 @@ import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
@@ -33,7 +33,6 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
@@ -97,7 +96,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
|
||||
|
||||
Resources resources = this.getResources();
|
||||
|
||||
GlideApp.with(this)
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(contactPhoto)
|
||||
.fallback(fallbackPhoto.asCallCard(this))
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
@@ -18,7 +17,6 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
import org.thoughtcrime.securesms.util.ConfigurationUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import com.bumptech.glide.RequestManager;
|
||||
|
||||
import org.signal.ringrtc.CallLinkRootKey;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
@@ -26,7 +28,6 @@ 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.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
@@ -41,7 +42,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
@NonNull ConversationMessage messageRecord,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
@NonNull Optional<MessageRecord> nextMessageRecord,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull RequestManager requestManager,
|
||||
@NonNull Locale locale,
|
||||
@NonNull Set<MultiselectPart> batchSelected,
|
||||
@NonNull Recipient recipients,
|
||||
@@ -122,5 +123,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
|
||||
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
|
||||
void onShowSafetyTips(boolean forGroup);
|
||||
void onReportSpamLearnMoreClicked();
|
||||
void onMessageRequestAcceptOptionsClicked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ package org.thoughtcrime.securesms;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
|
||||
import com.bumptech.glide.RequestManager;
|
||||
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
@@ -14,10 +15,11 @@ public interface BindableConversationListItem extends Unbindable {
|
||||
|
||||
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ThreadRecord thread,
|
||||
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
|
||||
@NonNull RequestManager requestManager, @NonNull Locale locale,
|
||||
@NonNull Set<Long> typingThreads,
|
||||
@NonNull ConversationSet selectedConversations);
|
||||
|
||||
void setSelectedConversations(@NonNull ConversationSet conversations);
|
||||
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
|
||||
void updateTimestamp();
|
||||
}
|
||||
|
||||
@@ -11,16 +11,36 @@ import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import okio.ByteString;
|
||||
|
||||
/**
|
||||
* This should be used whenever we want to prompt the user to block/unblock a recipient.
|
||||
*/
|
||||
public final class BlockUnblockDialog {
|
||||
|
||||
private BlockUnblockDialog() { }
|
||||
private BlockUnblockDialog() {}
|
||||
|
||||
public static void showReportSpamFor(@NonNull Context context,
|
||||
@NonNull Lifecycle lifecycle,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull Runnable onReportSpam,
|
||||
@Nullable Runnable onBlockAndReportSpam)
|
||||
{
|
||||
SimpleTask.run(lifecycle,
|
||||
() -> buildReportSpamFor(context, recipient, onReportSpam, onBlockAndReportSpam),
|
||||
AlertDialog.Builder::show);
|
||||
}
|
||||
|
||||
public static void showBlockFor(@NonNull Context context,
|
||||
@NonNull Lifecycle lifecycle,
|
||||
@@ -137,4 +157,37 @@ public final class BlockUnblockDialog {
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static AlertDialog.Builder buildReportSpamFor(@NonNull Context context,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull Runnable onReportSpam,
|
||||
@Nullable Runnable onBlockAndReportSpam)
|
||||
{
|
||||
recipient = recipient.resolve();
|
||||
|
||||
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.BlockUnblockDialog_report_spam_title)
|
||||
.setPositiveButton(R.string.BlockUnblockDialog_report_spam, (d, w) -> onReportSpam.run());
|
||||
|
||||
if (onBlockAndReportSpam != null) {
|
||||
builder.setNeutralButton(android.R.string.cancel, null)
|
||||
.setNegativeButton(R.string.BlockUnblockDialog_report_spam_and_block, (d, w) -> onBlockAndReportSpam.run());
|
||||
} else {
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
Recipient adder = SignalDatabase.groups().getGroupInviter(recipient.requireGroupId());
|
||||
if (adder != null) {
|
||||
builder.setMessage(context.getString(R.string.BlockUnblockDialog_report_spam_group_named_adder, adder.getDisplayName(context)));
|
||||
} else {
|
||||
builder.setMessage(R.string.BlockUnblockDialog_report_spam_group_unknown_adder);
|
||||
}
|
||||
} else {
|
||||
builder.setMessage(R.string.BlockUnblockDialog_report_spam_description);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,17 @@ import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DisplayMetricsUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
@@ -70,8 +73,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle, boolean ready) {
|
||||
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
|
||||
boolean includeSms = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||
int displayMode = includeSms ? ContactSelectionDisplayMode.FLAG_ALL : ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
|
||||
int displayMode = ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
|
||||
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
|
||||
}
|
||||
|
||||
@@ -99,6 +101,10 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
|
||||
|
||||
private void initializeContactFilterView() {
|
||||
this.contactFilterView = findViewById(R.id.contact_filter_edit_text);
|
||||
|
||||
if (getResources().getDisplayMetrics().heightPixels >= DimensionUnit.DP.toPixels(600)) {
|
||||
this.contactFilterView.focusAndShowKeyboard();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
|
||||
@@ -27,6 +27,8 @@ class ContactSelectionListAdapter(
|
||||
registerFactory(RefreshContactsModel::class.java, LayoutFactory({ RefreshContactsViewHolder(it, onClickCallbacks::onRefreshContactsClicked) }, R.layout.contact_selection_refresh_action_item))
|
||||
registerFactory(MoreHeaderModel::class.java, LayoutFactory({ MoreHeaderViewHolder(it) }, R.layout.contact_search_section_header))
|
||||
registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.contact_selection_empty_state))
|
||||
registerFactory(FindByUsernameModel::class.java, LayoutFactory({ FindByUsernameViewHolder(it, onClickCallbacks::onFindByUsernameClicked) }, R.layout.contact_selection_find_by_username_item))
|
||||
registerFactory(FindByPhoneNumberModel::class.java, LayoutFactory({ FindByPhoneNumberViewHolder(it, onClickCallbacks::onFindByPhoneNumberClicked) }, R.layout.contact_selection_find_by_phone_number_item))
|
||||
}
|
||||
|
||||
class NewGroupModel : MappingModel<NewGroupModel> {
|
||||
@@ -44,6 +46,16 @@ class ContactSelectionListAdapter(
|
||||
override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true
|
||||
}
|
||||
|
||||
class FindByUsernameModel : MappingModel<FindByUsernameModel> {
|
||||
override fun areItemsTheSame(newItem: FindByUsernameModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: FindByUsernameModel): Boolean = true
|
||||
}
|
||||
|
||||
class FindByPhoneNumberModel : MappingModel<FindByPhoneNumberModel> {
|
||||
override fun areItemsTheSame(newItem: FindByPhoneNumberModel): Boolean = true
|
||||
override fun areContentsTheSame(newItem: FindByPhoneNumberModel): Boolean = true
|
||||
}
|
||||
|
||||
class MoreHeaderModel : MappingModel<MoreHeaderModel> {
|
||||
override fun areItemsTheSame(newItem: MoreHeaderModel): Boolean = true
|
||||
|
||||
@@ -92,13 +104,33 @@ class ContactSelectionListAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
private class FindByPhoneNumberViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<FindByPhoneNumberModel>(itemView) {
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener { onClickListener() }
|
||||
}
|
||||
|
||||
override fun bind(model: FindByPhoneNumberModel) = Unit
|
||||
}
|
||||
|
||||
private class FindByUsernameViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<FindByUsernameModel>(itemView) {
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener { onClickListener() }
|
||||
}
|
||||
|
||||
override fun bind(model: FindByUsernameModel) = Unit
|
||||
}
|
||||
|
||||
class ArbitraryRepository : org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository {
|
||||
|
||||
enum class ArbitraryRow(val code: String) {
|
||||
NEW_GROUP("new-group"),
|
||||
INVITE_TO_SIGNAL("invite-to-signal"),
|
||||
MORE_HEADING("more-heading"),
|
||||
REFRESH_CONTACTS("refresh-contacts");
|
||||
REFRESH_CONTACTS("refresh-contacts"),
|
||||
FIND_BY_USERNAME("find-by-username"),
|
||||
FIND_BY_PHONE_NUMBER("find-by-phone-number");
|
||||
|
||||
companion object {
|
||||
fun fromCode(code: String) = values().first { it.code == code }
|
||||
@@ -120,6 +152,8 @@ class ContactSelectionListAdapter(
|
||||
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
|
||||
ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
|
||||
ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
|
||||
ArbitraryRow.FIND_BY_PHONE_NUMBER -> FindByPhoneNumberModel()
|
||||
ArbitraryRow.FIND_BY_USERNAME -> FindByUsernameModel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,5 +162,7 @@ class ContactSelectionListAdapter(
|
||||
fun onNewGroupClicked()
|
||||
fun onInviteToSignalClicked()
|
||||
fun onRefreshContactsClicked()
|
||||
fun onFindByPhoneNumberClicked()
|
||||
fun onFindByUsernameClicked()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
@@ -50,6 +49,8 @@ import androidx.transition.TransitionManager;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.signal.core.util.concurrent.RxExtensions;
|
||||
import org.signal.core.util.concurrent.SimpleTask;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
|
||||
@@ -70,10 +71,12 @@ import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
|
||||
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameAciFetchResult;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -85,6 +88,7 @@ import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
@@ -138,6 +142,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
private ContactSearchMediator contactSearchMediator;
|
||||
|
||||
@Nullable private NewConversationCallback newConversationCallback;
|
||||
@Nullable private FindByCallback findByCallback;
|
||||
@Nullable private NewCallCallback newCallCallback;
|
||||
@Nullable private ScrollCallback scrollCallback;
|
||||
@Nullable private OnItemLongClickListener onItemLongClickListener;
|
||||
@@ -145,6 +150,8 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
private Set<RecipientId> currentSelection;
|
||||
private boolean isMulti;
|
||||
private boolean canSelectSelf;
|
||||
private boolean resetPositionOnCommit = false;
|
||||
|
||||
private ListClickListener listClickListener = new ListClickListener();
|
||||
@Nullable private SwipeRefreshLayout.OnRefreshListener onRefreshListener;
|
||||
|
||||
@@ -156,6 +163,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
newConversationCallback = (NewConversationCallback) context;
|
||||
}
|
||||
|
||||
if (context instanceof FindByCallback) {
|
||||
findByCallback = (FindByCallback) context;
|
||||
}
|
||||
|
||||
if (context instanceof NewCallCallback) {
|
||||
newCallCallback = (NewCallCallback) context;
|
||||
}
|
||||
@@ -374,6 +385,16 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
newConversationCallback.onNewGroup(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindByPhoneNumberClicked() {
|
||||
findByCallback.onFindByPhoneNumber();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindByUsernameClicked() {
|
||||
findByCallback.onFindByUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInviteToSignalClicked() {
|
||||
if (newConversationCallback != null) {
|
||||
@@ -423,6 +444,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
onRefreshListener = null;
|
||||
}
|
||||
|
||||
public int getSelectedMembersSize() {
|
||||
return contactSearchMediator.getSelectedMembersSize();
|
||||
}
|
||||
|
||||
private @NonNull Bundle safeArguments() {
|
||||
return getArguments() != null ? getArguments() : new Bundle();
|
||||
}
|
||||
@@ -519,12 +544,21 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
}
|
||||
|
||||
public void setQueryFilter(String filter) {
|
||||
this.cursorFilter = filter;
|
||||
if (Objects.equals(filter, this.cursorFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetPositionOnCommit = true;
|
||||
this.cursorFilter = filter;
|
||||
|
||||
contactSearchMediator.onFilterChanged(filter);
|
||||
}
|
||||
|
||||
public void resetQueryFilter() {
|
||||
setQueryFilter(null);
|
||||
|
||||
this.resetPositionOnCommit = true;
|
||||
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
@@ -542,11 +576,12 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
headerActionView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void setRecyclerViewPaddingBottom(@Px int paddingBottom) {
|
||||
ViewUtil.setPaddingBottom(recyclerView, paddingBottom);
|
||||
}
|
||||
|
||||
private void onLoadFinished(int count) {
|
||||
if (resetPositionOnCommit) {
|
||||
resetPositionOnCommit = false;
|
||||
recyclerView.scrollToPosition(0);
|
||||
}
|
||||
|
||||
swipeRefresh.setVisibility(View.VISIBLE);
|
||||
showContactsLayout.setVisibility(View.GONE);
|
||||
|
||||
@@ -641,6 +676,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
public void addRecipientToSelectionIfAble(@NonNull RecipientId recipientId) {
|
||||
listClickListener.onItemClick(new ContactSearchKey.RecipientSearchKey(recipientId, false));
|
||||
}
|
||||
|
||||
private class ListClickListener {
|
||||
public void onItemClick(ContactSearchKey contact) {
|
||||
boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey;
|
||||
@@ -666,11 +705,18 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
|
||||
|
||||
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
|
||||
return UsernameUtil.fetchAciForUsername(username);
|
||||
}, uuid -> {
|
||||
try {
|
||||
return RxExtensions.safeBlockingGet(UsernameRepository.fetchAciForUsername(UsernameUtil.sanitizeUsernameFromSearch(username)));
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Interrupted?", e);
|
||||
return UsernameAciFetchResult.NetworkError.INSTANCE;
|
||||
}
|
||||
}, result -> {
|
||||
loadingDialog.dismiss();
|
||||
if (uuid.isPresent()) {
|
||||
Recipient recipient = Recipient.externalUsername(uuid.get(), username);
|
||||
|
||||
// TODO Could be more specific with errors
|
||||
if (result instanceof UsernameAciFetchResult.Success success) {
|
||||
Recipient recipient = Recipient.externalUsername(success.getAci(), username);
|
||||
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), username);
|
||||
|
||||
if (onContactSelectedListener != null) {
|
||||
@@ -844,10 +890,15 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
return ContactSearchConfiguration.build(builder -> {
|
||||
builder.setQuery(contactSearchState.getQuery());
|
||||
|
||||
if (newConversationCallback != null) {
|
||||
if (newConversationCallback != null && !hasQuery) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
|
||||
}
|
||||
|
||||
if (findByCallback != null && !hasQuery) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_USERNAME.getCode());
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_PHONE_NUMBER.getCode());
|
||||
}
|
||||
|
||||
if (transportType != null) {
|
||||
if (!hasQuery && includeRecents) {
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Recents(
|
||||
@@ -862,12 +913,14 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
));
|
||||
}
|
||||
|
||||
boolean hideHeader = newCallCallback != null || (newConversationCallback != null && !hasQuery);
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
|
||||
includeSelf,
|
||||
transportType,
|
||||
newCallCallback == null,
|
||||
!hideHeader,
|
||||
null,
|
||||
!hideLetterHeaders()
|
||||
!hideLetterHeaders(),
|
||||
newConversationCallback != null ? ContactSearchSortOrder.RECENCY : ContactSearchSortOrder.NATURAL
|
||||
));
|
||||
}
|
||||
|
||||
@@ -893,7 +946,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
builder.username(newRowMode);
|
||||
}
|
||||
|
||||
if (newCallCallback != null || newConversationCallback != null) {
|
||||
if ((newCallCallback != null || newConversationCallback != null) && !hasQuery) {
|
||||
addMoreSection(builder);
|
||||
builder.withEmptyState(emptyBuilder -> {
|
||||
emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
|
||||
@@ -985,6 +1038,12 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
void onNewGroup(boolean forceV1);
|
||||
}
|
||||
|
||||
public interface FindByCallback {
|
||||
void onFindByUsername();
|
||||
|
||||
void onFindByPhoneNumber();
|
||||
}
|
||||
|
||||
public interface NewCallCallback {
|
||||
void onInvite();
|
||||
}
|
||||
|
||||
@@ -3,15 +3,14 @@ package org.thoughtcrime.securesms;
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.text.TextUtils;
|
||||
import android.transition.TransitionInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -30,9 +29,10 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.qr.kitkat.ScanListener;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.signal.core.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
@@ -50,6 +50,8 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
|
||||
private static final String TAG = Log.tag(DeviceActivity.class);
|
||||
|
||||
private static final String EXTRA_DIRECT_TO_SCANNER = "add";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
@@ -58,6 +60,13 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
private DeviceLinkFragment deviceLinkFragment;
|
||||
private MenuItem cameraSwitchItem = null;
|
||||
|
||||
|
||||
public static Intent getIntentForScanner(Context context) {
|
||||
Intent intent = new Intent(context, DeviceActivity.class);
|
||||
intent.putExtra(EXTRA_DIRECT_TO_SCANNER, true);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
@@ -81,7 +90,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
this.deviceListFragment.setAddDeviceButtonListener(this);
|
||||
this.deviceAddFragment.setScanListener(this);
|
||||
|
||||
if (getIntent().getBooleanExtra("add", false)) {
|
||||
if (getIntent().getBooleanExtra(EXTRA_DIRECT_TO_SCANNER, false)) {
|
||||
initFragment(R.id.fragment_container, deviceAddFragment, dynamicLanguage.getCurrentLocale());
|
||||
} else {
|
||||
initFragment(R.id.fragment_container, deviceListFragment, dynamicLanguage.getCurrentLocale());
|
||||
@@ -197,7 +206,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
ProfileKey profileKey = ProfileKeyUtil.getSelfProfileKey();
|
||||
|
||||
TextSecurePreferences.setMultiDevice(DeviceActivity.this, true);
|
||||
accountManager.addDevice(ephemeralId, publicKey, aciIdentityKeyPair, pniIdentityKeyPair, profileKey, verificationCode);
|
||||
accountManager.addDevice(ephemeralId, publicKey, aciIdentityKeyPair, pniIdentityKeyPair, profileKey, SignalStore.svr().getOrCreateMasterKey(), verificationCode);
|
||||
|
||||
return SUCCESS;
|
||||
} catch (NotFoundException e) {
|
||||
@@ -223,6 +232,8 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
||||
protected void onPostExecute(Integer result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
LinkedDeviceInactiveCheckJob.enqueue();
|
||||
|
||||
Context context = DeviceActivity.this;
|
||||
|
||||
switch (result) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.devicelist.Device;
|
||||
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
@@ -166,6 +167,7 @@ public class DeviceListFragment extends ListFragment
|
||||
super.onPostExecute(result);
|
||||
if (result) {
|
||||
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
|
||||
LinkedDeviceInactiveCheckJob.enqueue();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@@ -26,9 +26,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
|
||||
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
|
||||
.setPositiveButton(R.string.DeviceProvisioningActivity_continue, (dialog1, which) -> {
|
||||
Intent intent = new Intent(DeviceProvisioningActivity.this, DeviceActivity.class);
|
||||
intent.putExtra("add", true);
|
||||
startActivity(intent);
|
||||
startActivity(DeviceActivity.getIntentForScanner(this));
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog12, which) -> {
|
||||
@@ -38,7 +36,6 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
.setOnDismissListener(dialog13 -> finish())
|
||||
.create();
|
||||
|
||||
dialog.setIcon(getResources().getDrawable(R.drawable.icon_dialog));
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ public final class GroupMembersDialog {
|
||||
}
|
||||
|
||||
private void contactClick(@NonNull Recipient recipient) {
|
||||
RecipientBottomSheetDialogFragment.create(recipient.getId(), groupRecipient.requireGroupId())
|
||||
.show(fragmentActivity.getSupportFragmentManager(), "BOTTOM");
|
||||
RecipientBottomSheetDialogFragment.show(fragmentActivity.getSupportFragmentManager(), recipient.getId(), groupRecipient.requireGroupId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,16 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AnimRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView;
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
|
||||
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage;
|
||||
@@ -38,7 +37,6 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
|
||||
|
||||
@@ -121,14 +119,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
smsSendButton.setOnClickListener(new SmsSendClickListener());
|
||||
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
|
||||
|
||||
if (Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().isSmsSupported()) {
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
smsButton.setOnClickListener(new SmsClickListener());
|
||||
} else {
|
||||
smsButton.setVisibility(View.GONE);
|
||||
shareText.setText(R.string.InviteActivity_share);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
}
|
||||
smsButton.setVisibility(View.GONE);
|
||||
shareText.setText(R.string.InviteActivity_share);
|
||||
shareButton.setOnClickListener(new ShareClickListener());
|
||||
}
|
||||
|
||||
private Animation loadAnimation(@AnimRes int animResId) {
|
||||
@@ -202,13 +195,6 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
ViewUtil.animateIn(smsSendFrame, slideInAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
private class SmsCancelClickListener implements OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
@@ -17,15 +17,17 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.signal.donations.StripeApi;
|
||||
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment;
|
||||
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment;
|
||||
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
|
||||
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment;
|
||||
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
|
||||
import org.thoughtcrime.securesms.notifications.SlowNotificationsViewModel;
|
||||
import org.thoughtcrime.securesms.notifications.VitalsViewModel;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
|
||||
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
|
||||
import org.thoughtcrime.securesms.util.AppStartup;
|
||||
@@ -45,7 +47,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
|
||||
private VoiceNoteMediaController mediaController;
|
||||
private ConversationListTabsViewModel conversationListTabsViewModel;
|
||||
private SlowNotificationsViewModel slowNotificationsViewModel;
|
||||
private VitalsViewModel vitalsViewModel;
|
||||
|
||||
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
|
||||
|
||||
@@ -89,35 +91,34 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
ConversationListTabRepository repository = new ConversationListTabRepository();
|
||||
ConversationListTabsViewModel.Factory factory = new ConversationListTabsViewModel.Factory(repository);
|
||||
|
||||
handleGroupLinkInIntent(getIntent());
|
||||
handleProxyInIntent(getIntent());
|
||||
handleSignalMeIntent(getIntent());
|
||||
handleCallLinkInIntent(getIntent());
|
||||
handleDeeplinkIntent(getIntent());
|
||||
|
||||
CachedInflater.from(this).clear();
|
||||
|
||||
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
|
||||
updateTabVisibility();
|
||||
|
||||
slowNotificationsViewModel = new ViewModelProvider(this).get(SlowNotificationsViewModel.class);
|
||||
vitalsViewModel = new ViewModelProvider(this).get(VitalsViewModel.class);
|
||||
|
||||
lifecycleDisposable.add(
|
||||
slowNotificationsViewModel
|
||||
.getSlowNotificationState()
|
||||
.subscribe(this::presentSlowNotificationState)
|
||||
vitalsViewModel
|
||||
.getVitalsState()
|
||||
.subscribe(this::presentVitalsState)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private void presentSlowNotificationState(SlowNotificationsViewModel.State slowNotificationState) {
|
||||
switch (slowNotificationState) {
|
||||
private void presentVitalsState(VitalsViewModel.State state) {
|
||||
switch (state) {
|
||||
case NONE:
|
||||
break;
|
||||
case PROMPT_BATTERY_SAVER_DIALOG:
|
||||
PromptBatterySaverDialogFragment.show(getSupportFragmentManager());
|
||||
break;
|
||||
case PROMPT_DEBUGLOGS:
|
||||
DebugLogsPromptDialogFragment.show(this, getSupportFragmentManager());
|
||||
case PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS:
|
||||
DebugLogsPromptDialogFragment.show(this, DebugLogsPromptDialogFragment.Purpose.NOTIFICATIONS);
|
||||
case PROMPT_DEBUGLOGS_FOR_CRASH:
|
||||
DebugLogsPromptDialogFragment.show(this, DebugLogsPromptDialogFragment.Purpose.CRASH);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -132,10 +133,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
handleGroupLinkInIntent(intent);
|
||||
handleProxyInIntent(intent);
|
||||
handleSignalMeIntent(intent);
|
||||
handleCallLinkInIntent(intent);
|
||||
handleDeeplinkIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -154,7 +152,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
.setMessage(R.string.OldDeviceTransferLockedDialog__your_signal_account_has_been_transferred_to_your_new_device)
|
||||
.setPositiveButton(R.string.OldDeviceTransferLockedDialog__done, (d, w) -> OldDeviceExitActivity.exit(this))
|
||||
.setNegativeButton(R.string.OldDeviceTransferLockedDialog__cancel_and_activate_this_device, (d, w) -> {
|
||||
SignalStore.misc().clearOldDeviceTransferLocked();
|
||||
SignalStore.misc().setOldDeviceTransferLocked(false);
|
||||
DeviceTransferBlockingInterceptor.getInstance().unblockNetwork();
|
||||
})
|
||||
.setCancelable(false)
|
||||
@@ -168,7 +166,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
|
||||
updateTabVisibility();
|
||||
|
||||
slowNotificationsViewModel.checkSlowNotificationHeuristics();
|
||||
vitalsViewModel.checkSlowNotificationHeuristics();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -201,6 +199,14 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
return navigator;
|
||||
}
|
||||
|
||||
private void handleDeeplinkIntent(Intent intent) {
|
||||
handleGroupLinkInIntent(intent);
|
||||
handleProxyInIntent(intent);
|
||||
handleSignalMeIntent(intent);
|
||||
handleCallLinkInIntent(intent);
|
||||
handleDonateReturnIntent(intent);
|
||||
}
|
||||
|
||||
private void handleGroupLinkInIntent(Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
@@ -229,6 +235,13 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDonateReturnIntent(Intent intent) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null && data.toString().startsWith(StripeApi.RETURN_URL_IDEAL)) {
|
||||
startActivity(AppSettingsActivity.manageSubscriptions(this));
|
||||
}
|
||||
}
|
||||
|
||||
public void onFirstRender() {
|
||||
onFirstRender = true;
|
||||
}
|
||||
|
||||
@@ -50,8 +50,10 @@ import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientRepository;
|
||||
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity;
|
||||
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -71,14 +73,16 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class NewConversationActivity extends ContactSelectionActivity
|
||||
implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener
|
||||
implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener, ContactSelectionListFragment.FindByCallback
|
||||
{
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(NewConversationActivity.class);
|
||||
|
||||
private ContactsManagementViewModel viewModel;
|
||||
private ActivityResultLauncher<Intent> contactLauncher;
|
||||
private ContactsManagementViewModel viewModel;
|
||||
private ActivityResultLauncher<Intent> contactLauncher;
|
||||
private ActivityResultLauncher<Intent> createGroupLauncher;
|
||||
private ActivityResultLauncher<FindByMode> findByLauncher;
|
||||
|
||||
private final LifecycleDisposable disposables = new LifecycleDisposable();
|
||||
|
||||
@@ -100,13 +104,23 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
});
|
||||
|
||||
findByLauncher = registerForActivityResult(new FindByActivity.Contract(), result -> {
|
||||
if (result != null) {
|
||||
launch(result);
|
||||
}
|
||||
});
|
||||
|
||||
createGroupLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
viewModel = new ViewModelProvider(this, factory).get(ContactsManagementViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
|
||||
boolean smsSupported = SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
|
||||
|
||||
if (recipientId.isPresent()) {
|
||||
launch(Recipient.resolved(recipientId.get()));
|
||||
} else {
|
||||
@@ -117,33 +131,19 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
|
||||
AlertDialog progress = SimpleProgressDialog.show(this);
|
||||
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Recipient resolved = Recipient.external(this, number);
|
||||
|
||||
if (!resolved.isRegistered() || !resolved.hasServiceId()) {
|
||||
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
|
||||
try {
|
||||
ContactDiscovery.refresh(this, resolved, false, TimeUnit.SECONDS.toMillis(10));
|
||||
resolved = Recipient.resolved(resolved.getId());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}, resolved -> {
|
||||
SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(this, number), result -> {
|
||||
progress.dismiss();
|
||||
|
||||
if (resolved != null) {
|
||||
if (smsSupported || resolved.isRegistered() && resolved.hasServiceId()) {
|
||||
if (result instanceof RecipientRepository.LookupResult.Success) {
|
||||
Recipient resolved = Recipient.resolved(((RecipientRepository.LookupResult.Success) result).getRecipientId());
|
||||
if (resolved.isRegistered() && resolved.hasServiceId()) {
|
||||
launch(resolved);
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, resolved.getDisplayName(this)))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
} else if (result instanceof RecipientRepository.LookupResult.NotFound || result instanceof RecipientRepository.LookupResult.InvalidEntry) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, number))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
} else {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again)
|
||||
@@ -151,8 +151,6 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else if (smsSupported) {
|
||||
launch(Recipient.external(this, number));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +162,12 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
private void launch(Recipient recipient) {
|
||||
Disposable disposable = ConversationIntents.createBuilder(this, recipient.getId(), -1L)
|
||||
launch(recipient.getId());
|
||||
}
|
||||
|
||||
|
||||
private void launch(RecipientId recipientId) {
|
||||
Disposable disposable = ConversationIntents.createBuilder(this, recipientId, -1L)
|
||||
.map(builder -> builder
|
||||
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
|
||||
.withDataUri(getIntent().getData())
|
||||
@@ -207,7 +210,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
private void handleCreateGroup() {
|
||||
startActivity(CreateGroupActivity.newIntent(this));
|
||||
createGroupLauncher.launch(CreateGroupActivity.newIntent(this));
|
||||
}
|
||||
|
||||
private void handleInvite() {
|
||||
@@ -232,7 +235,17 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
@Override
|
||||
public void onNewGroup(boolean forceV1) {
|
||||
handleCreateGroup();
|
||||
finish();
|
||||
// finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindByUsername() {
|
||||
findByLauncher.launch(FindByMode.USERNAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindByPhoneNumber() {
|
||||
findByLauncher.launch(FindByMode.PHONE_NUMBER);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -287,7 +300,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
return null;
|
||||
}
|
||||
|
||||
if (recipient.isRegistered() || (SignalStore.misc().getSmsExportPhase().allowSmsFeatures())) {
|
||||
if (recipient.isRegistered()) {
|
||||
return new ActionItem(
|
||||
R.drawable.ic_phone_right_24,
|
||||
getString(R.string.NewConversationActivity__audio_call),
|
||||
@@ -313,7 +326,7 @@ public class NewConversationActivity extends ContactSelectionActivity
|
||||
}
|
||||
|
||||
private @Nullable ActionItem createRemoveActionItem(@NonNull Recipient recipient) {
|
||||
if (!FeatureFlags.hideContacts() || recipient.isSelf() || recipient.isGroup()) {
|
||||
if (recipient.isSelf() || recipient.isGroup()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||