Compare commits
837 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0775fc7ead | ||
|
|
034aef483b | ||
|
|
ee5b99fed4 | ||
|
|
e031da1337 | ||
|
|
8f1514642c | ||
|
|
5aa304ea9a | ||
|
|
c5a27b2cc7 | ||
|
|
0fde404da8 | ||
|
|
5242b9af39 | ||
|
|
5e2d6fc05f | ||
|
|
7e08a1f321 | ||
|
|
076295eae8 | ||
|
|
c13339ca52 | ||
|
|
627657e1de | ||
|
|
a8349671d0 | ||
|
|
461875b0e4 | ||
|
|
00bbb6bc6e | ||
|
|
e1f1181a07 | ||
|
|
0e1de39192 | ||
|
|
05bbeb10da | ||
|
|
7375a9e06b | ||
|
|
4910050891 | ||
|
|
67d4f666ce | ||
|
|
e6c9449e3c | ||
|
|
b8effba497 | ||
|
|
8fcdd7cb8a | ||
|
|
f3fb5ccc3b | ||
|
|
b8f55f982f | ||
|
|
6db59cb896 | ||
|
|
3db83c1602 | ||
|
|
6be9225fbd | ||
|
|
653eff403c | ||
|
|
ab410ec0cf | ||
|
|
7b75a32394 | ||
|
|
daf077b3c9 | ||
|
|
f6bbb59400 | ||
|
|
09813d5dbd | ||
|
|
fe509838f4 | ||
|
|
6a443d0074 | ||
|
|
8fc1065dd6 | ||
|
|
1af50ba0f5 | ||
|
|
82b3036b77 | ||
|
|
676412019c | ||
|
|
980f4e00e2 | ||
|
|
5731bf023a | ||
|
|
2511ca17aa | ||
|
|
fae653540b | ||
|
|
b0ca66cc1a | ||
|
|
a65e9c76bc | ||
|
|
adbac4c557 | ||
|
|
fc7b024e96 | ||
|
|
acee65ba25 | ||
|
|
244902ecfc | ||
|
|
4b1a678af2 | ||
|
|
59dd72b5c0 | ||
|
|
44c393f11a | ||
|
|
fddfbd8d2d | ||
|
|
e7e00bd428 | ||
|
|
07702e69ad | ||
|
|
e5c3757629 | ||
|
|
7031bbae43 | ||
|
|
4a6dfed676 | ||
|
|
bb0b414d71 | ||
|
|
95e25652c1 | ||
|
|
58155b0859 | ||
|
|
f579b79d2e | ||
|
|
47673be4e0 | ||
|
|
f67e6a9e9f | ||
|
|
110d8259fa | ||
|
|
8f253ffc43 | ||
|
|
6ca9cb6da1 | ||
|
|
1b63bdec12 | ||
|
|
bb52172516 | ||
|
|
1640495f34 | ||
|
|
64415a980f | ||
|
|
e06d141823 | ||
|
|
ac4d8679a1 | ||
|
|
8fc03a67b9 | ||
|
|
648506fe04 | ||
|
|
2c74ac8bfa | ||
|
|
963709c552 | ||
|
|
9af888a595 | ||
|
|
ec373b5b4d | ||
|
|
bfcd57881e | ||
|
|
b277a8c5e0 | ||
|
|
979a50716e | ||
|
|
c5f0da8151 | ||
|
|
aee0b5268f | ||
|
|
7e909f2bee | ||
|
|
584c90521a | ||
|
|
23ef8c78bd | ||
|
|
5ca025544e | ||
|
|
500ae0c72e | ||
|
|
c359207f1f | ||
|
|
159f2ebec0 | ||
|
|
a0db812606 | ||
|
|
d4c6a433d7 | ||
|
|
3a7cde9239 | ||
|
|
4b6c308ae9 | ||
|
|
002279f6a7 | ||
|
|
c807e52ad9 | ||
|
|
9557e3b910 | ||
|
|
ddc77884bd | ||
|
|
9d6337d5a8 | ||
|
|
117dd17215 | ||
|
|
f9eed0f6d0 | ||
|
|
4429145cdf | ||
|
|
5ea4cbf9ca | ||
|
|
c6473ca9e6 | ||
|
|
38b2a2f5b7 | ||
|
|
ebaa445bee | ||
|
|
8372c699f7 | ||
|
|
e1570e9512 | ||
|
|
dfb7304626 | ||
|
|
919531a82b | ||
|
|
81b2e9ccd2 | ||
|
|
4ef2aba4e2 | ||
|
|
4590655dc5 | ||
|
|
3040b70100 | ||
|
|
47b97aafc6 | ||
|
|
27e7383db6 | ||
|
|
42fe827cb3 | ||
|
|
3fa3e8357c | ||
|
|
6260607e1b | ||
|
|
3c1666e874 | ||
|
|
f4a082584c | ||
|
|
87d4dba32b | ||
|
|
329f68d167 | ||
|
|
2053cf085a | ||
|
|
16d48984c5 | ||
|
|
a17800283a | ||
|
|
9b1917cbdc | ||
|
|
e1e3d7a85b | ||
|
|
dc37d1f029 | ||
|
|
53e62f2be0 | ||
|
|
e6cc789c6f | ||
|
|
cfaef77b21 | ||
|
|
36fc9aa82a | ||
|
|
8d20669e46 | ||
|
|
9d8501cd64 | ||
|
|
88827e94f5 | ||
|
|
99d2a0c0b6 | ||
|
|
1d0582867b | ||
|
|
0ea6d9205d | ||
|
|
f438ef543b | ||
|
|
61cd9767c8 | ||
|
|
5fbf0a98b9 | ||
|
|
1671518ded | ||
|
|
f628ffca06 | ||
|
|
1d6f4fd4e7 | ||
|
|
9eedc0a36b | ||
|
|
a870fe9e1a | ||
|
|
d3f779cea9 | ||
|
|
fb4f41b996 | ||
|
|
11aac76fb6 | ||
|
|
98424f6cbb | ||
|
|
1cf7c59af9 | ||
|
|
13470fb0c3 | ||
|
|
3aa0fd1937 | ||
|
|
9e6f2336d1 | ||
|
|
8b8d62f598 | ||
|
|
4572ae5886 | ||
|
|
0802d4beb4 | ||
|
|
9361aa700a | ||
|
|
be5cad1cec | ||
|
|
fe20de2995 | ||
|
|
8714e4298e | ||
|
|
1baebe7475 | ||
|
|
6686ae43f3 | ||
|
|
cc2c0e9561 | ||
|
|
34d252a4bd | ||
|
|
025411c9fb | ||
|
|
4edb66d2b9 | ||
|
|
a17033dff4 | ||
|
|
04a5e56da7 | ||
|
|
0e6a3dd408 | ||
|
|
47f48a6a8c | ||
|
|
2ef7fabade | ||
|
|
5c2b475c01 | ||
|
|
559f4bc0d3 | ||
|
|
1357a4816b | ||
|
|
d1b8a56c0f | ||
|
|
7ea38298ea | ||
|
|
c08f1355db | ||
|
|
b6589637fa | ||
|
|
2ee2d2883a | ||
|
|
029c8ba917 | ||
|
|
60e1ee21ed | ||
|
|
a96e9158c4 | ||
|
|
e47765d7d5 | ||
|
|
e807435c8b | ||
|
|
14b41a93e2 | ||
|
|
f51fb9da29 | ||
|
|
df3ca3d3cc | ||
|
|
7786956b11 | ||
|
|
c17d62aeab | ||
|
|
65255121de | ||
|
|
b042945fef | ||
|
|
9d979217fa | ||
|
|
c177de2ec3 | ||
|
|
b4fd57d900 | ||
|
|
92339dfdcf | ||
|
|
8ae115028e | ||
|
|
2dd0221680 | ||
|
|
1a1923c6c0 | ||
|
|
5801ad4bdb | ||
|
|
388f2971e9 | ||
|
|
14c3a36ec0 | ||
|
|
1ad338ce31 | ||
|
|
71981e8a27 | ||
|
|
5cb10cd054 | ||
|
|
ecf576e9b9 | ||
|
|
09d17659b9 | ||
|
|
53673be5cb | ||
|
|
ed4a1d6ddd | ||
|
|
e3044b8b85 | ||
|
|
4ce05a064c | ||
|
|
2fbcc23451 | ||
|
|
3bdffed8c9 | ||
|
|
6cc8e87d46 | ||
|
|
7d8f549d97 | ||
|
|
fb2ef265bd | ||
|
|
96e8256781 | ||
|
|
dd40517f12 | ||
|
|
6c95b766d6 | ||
|
|
feffdcb71e | ||
|
|
f33e2c49ca | ||
|
|
1037acd4a2 | ||
|
|
68042fc755 | ||
|
|
1bdc77affe | ||
|
|
88f50da4fb | ||
|
|
bc1fbd9b6c | ||
|
|
c1b7b7c95e | ||
|
|
b0688eed5c | ||
|
|
e06ed03d33 | ||
|
|
e2a79394ab | ||
|
|
5db770ca44 | ||
|
|
df8aaa2005 | ||
|
|
882748f080 | ||
|
|
15035f4eb3 | ||
|
|
1d0a87f52a | ||
|
|
59b2cc5f79 | ||
|
|
d6758fc264 | ||
|
|
36418bec59 | ||
|
|
07bd8b2fa3 | ||
|
|
d989d02af9 | ||
|
|
503ce13122 | ||
|
|
8f77321adb | ||
|
|
60cdcea791 | ||
|
|
86cd4c5c30 | ||
|
|
62d5f61a0b | ||
|
|
25860867bb | ||
|
|
272860f071 | ||
|
|
767cfbc717 | ||
|
|
55af6ca84e | ||
|
|
88933ae051 | ||
|
|
4781beebee | ||
|
|
0049c74323 | ||
|
|
361727cec6 | ||
|
|
1b95177e0e | ||
|
|
8d34c54de2 | ||
|
|
2fa0eba3db | ||
|
|
6cc41e95c6 | ||
|
|
b1523f5b91 | ||
|
|
d16002546d | ||
|
|
186a93f5d1 | ||
|
|
3d4875bcfe | ||
|
|
441e30971a | ||
|
|
ff115c2349 | ||
|
|
f23e5bdb44 | ||
|
|
d0a232d86a | ||
|
|
9fef8386e6 | ||
|
|
b1680ba5c6 | ||
|
|
6cd59daf0a | ||
|
|
38f2b39ac4 | ||
|
|
51222738df | ||
|
|
b9835584d8 | ||
|
|
cff01021c2 | ||
|
|
d19b8a125c | ||
|
|
4caaa0033b | ||
|
|
f3a0a059ea | ||
|
|
290b0fe46f | ||
|
|
03a212eee4 | ||
|
|
f3b629bc06 | ||
|
|
b9e002f7b1 | ||
|
|
c90779beea | ||
|
|
bc8c8a049f | ||
|
|
1d9dc66265 | ||
|
|
886c149c3f | ||
|
|
369ca189d3 | ||
|
|
02c4bbe816 | ||
|
|
523c9f6576 | ||
|
|
c259430b09 | ||
|
|
fc94b90a03 | ||
|
|
6af521130d | ||
|
|
9c001e4f35 | ||
|
|
9e8dee36a6 | ||
|
|
26b17d8a3c | ||
|
|
1c5e2e3359 | ||
|
|
0437d37f23 | ||
|
|
ec6b1a44de | ||
|
|
08c661bb14 | ||
|
|
62f62d89c5 | ||
|
|
37dd8b40b2 | ||
|
|
15825f6c3f | ||
|
|
ad196bf03c | ||
|
|
332c4ca26e | ||
|
|
305edf1928 | ||
|
|
9c0c25ef99 | ||
|
|
458dae227f | ||
|
|
b1ba9fd54f | ||
|
|
dd42b5b851 | ||
|
|
d8e3edc729 | ||
|
|
36aa8623da | ||
|
|
7e24252447 | ||
|
|
164ce06177 | ||
|
|
c9d298c447 | ||
|
|
5129613ce8 | ||
|
|
b985ace7ed | ||
|
|
d28afac973 | ||
|
|
fd9b5ff7c4 | ||
|
|
75580bea27 | ||
|
|
db7056c53b | ||
|
|
b55181ffe6 | ||
|
|
81149e5aa8 | ||
|
|
3a341eee19 | ||
|
|
e19c7efbfe | ||
|
|
7e7c68321b | ||
|
|
9fa3f54c7c | ||
|
|
3ff273f1f2 | ||
|
|
e607b1962c | ||
|
|
2c4c6bf87c | ||
|
|
bf048e2a75 | ||
|
|
b0c4bb04e7 | ||
|
|
7e0e6c2786 | ||
|
|
d6a03df087 | ||
|
|
cfc89d2a74 | ||
|
|
c491c9dc8c | ||
|
|
eae066b3a2 | ||
|
|
71aa17bad6 | ||
|
|
93df01e266 | ||
|
|
8f96abb41e | ||
|
|
1457a6fe16 | ||
|
|
290c107698 | ||
|
|
bf7aaddbf9 | ||
|
|
59435e49c8 | ||
|
|
c3499e538e | ||
|
|
1d41b1c5a3 | ||
|
|
e303570b2f | ||
|
|
62940893f0 | ||
|
|
f8434bede5 | ||
|
|
c08e108fc3 | ||
|
|
cd9a160cae | ||
|
|
bba8b8be56 | ||
|
|
f56b5d58c6 | ||
|
|
ac4b0ed606 | ||
|
|
d3e71185e6 | ||
|
|
b4f2cd9ff4 | ||
|
|
fd8d305899 | ||
|
|
bde7ae944a | ||
|
|
99f83e5dc9 | ||
|
|
693aef5c04 | ||
|
|
b9ae537706 | ||
|
|
e41dd6d39d | ||
|
|
5d546f46e4 | ||
|
|
2bef5653b4 | ||
|
|
63d8549865 | ||
|
|
f6bac2f476 | ||
|
|
0dd51856d3 | ||
|
|
be01f2b511 | ||
|
|
045d2cf42f | ||
|
|
64ddd982fe | ||
|
|
b785b3f887 | ||
|
|
399421e20e | ||
|
|
a656d65d1d | ||
|
|
291a5d57c4 | ||
|
|
a9a91e3162 | ||
|
|
de4c6ab7b7 | ||
|
|
7ea9fc0c3b | ||
|
|
1965d5879f | ||
|
|
b2b907a86a | ||
|
|
e565de0724 | ||
|
|
7318e676f7 | ||
|
|
7c28d8ad51 | ||
|
|
3e21fb77c7 | ||
|
|
6b91e525db | ||
|
|
0aca03a919 | ||
|
|
e2ef8e2ef9 | ||
|
|
15c248184f | ||
|
|
a0c1b072b6 | ||
|
|
fe806cc4eb | ||
|
|
aacad78cdb | ||
|
|
b89f2dd862 | ||
|
|
36d01477cc | ||
|
|
e4090d00c9 | ||
|
|
ff55bc8209 | ||
|
|
b375c9efdc | ||
|
|
67b4fde5c3 | ||
|
|
63c6581d14 | ||
|
|
3ddd01981d | ||
|
|
bbd845905a | ||
|
|
3f53c37187 | ||
|
|
159c0d1104 | ||
|
|
82db08b76f | ||
|
|
83e84228f5 | ||
|
|
05edc715ef | ||
|
|
c503df5eec | ||
|
|
1f242473fe | ||
|
|
f7db5f8ae0 | ||
|
|
3b88d7cf94 | ||
|
|
cac82f2eba | ||
|
|
5811b469cf | ||
|
|
af1175f32e | ||
|
|
7a5ce5761f | ||
|
|
34104355cb | ||
|
|
143c1255d8 | ||
|
|
a9d0e5ac81 | ||
|
|
c8b3ee51ed | ||
|
|
c964067139 | ||
|
|
539f590c4c | ||
|
|
71dddd4a1b | ||
|
|
792f5dd7b5 | ||
|
|
a3af49d92a | ||
|
|
4289b43a81 | ||
|
|
7f55623acf | ||
|
|
52060b65be | ||
|
|
fd826749e4 | ||
|
|
627e15c3dd | ||
|
|
90f6890180 | ||
|
|
61eb397d2b | ||
|
|
3a5e5364c7 | ||
|
|
25779d04a6 | ||
|
|
242900e87f | ||
|
|
05f07d1788 | ||
|
|
f961f4ccac | ||
|
|
145377b05f | ||
|
|
bc88887195 | ||
|
|
5362b1c21c | ||
|
|
0cfd3265ba | ||
|
|
1099128513 | ||
|
|
ad50c81a6b | ||
|
|
0817f113c6 | ||
|
|
4a9a07a9ef | ||
|
|
61f50cfe60 | ||
|
|
92888778c2 | ||
|
|
987f9b9dba | ||
|
|
97d95f37cc | ||
|
|
836cd04564 | ||
|
|
c26f54161d | ||
|
|
b540009ce6 | ||
|
|
e58e209950 | ||
|
|
4597a23104 | ||
|
|
3aacf4bcd2 | ||
|
|
6e6b663fac | ||
|
|
6dad7eafcf | ||
|
|
5a38143987 | ||
|
|
4d6d31d624 | ||
|
|
938c82be3f | ||
|
|
dc2e249566 | ||
|
|
bb8fdcabcb | ||
|
|
6cf4dbc78c | ||
|
|
8cd0ac5451 | ||
|
|
33745f0b0c | ||
|
|
29ffed219f | ||
|
|
9629d0f715 | ||
|
|
01651984d7 | ||
|
|
c7c15250ca | ||
|
|
36b96eafcc | ||
|
|
94f930ee22 | ||
|
|
3f740d2904 | ||
|
|
f8529adfcf | ||
|
|
77ccbdd322 | ||
|
|
f2846efd2c | ||
|
|
131f9c4bc9 | ||
|
|
d7c06fff50 | ||
|
|
09a22d9dc4 | ||
|
|
0fbab04253 | ||
|
|
c963e99dca | ||
|
|
7a555d127f | ||
|
|
866408f673 | ||
|
|
a7e5ab1a6a | ||
|
|
14d16d61e6 | ||
|
|
b988e4a813 | ||
|
|
ae33c8db1b | ||
|
|
9f1c5ac6bb | ||
|
|
448e7d0739 | ||
|
|
8971ff9057 | ||
|
|
2d6b16b2ce | ||
|
|
44ab1643fa | ||
|
|
a64bffd83a | ||
|
|
534c5c3c64 | ||
|
|
99d3f9918f | ||
|
|
aaebf029db | ||
|
|
e2c2ace0e3 | ||
|
|
c76002663f | ||
|
|
c5317370c8 | ||
|
|
4b09f4a654 | ||
|
|
0c57113d8e | ||
|
|
d7dd77a5af | ||
|
|
5c5b88ebcc | ||
|
|
8df0248d4f | ||
|
|
58e48fdf14 | ||
|
|
407fc56218 | ||
|
|
398527d3f1 | ||
|
|
59745a695c | ||
|
|
2aaeda6ca8 | ||
|
|
5c78de2f46 | ||
|
|
93efc21452 | ||
|
|
50ad005e7c | ||
|
|
71c3bcdd29 | ||
|
|
e5bf04a407 | ||
|
|
fe8b2cb761 | ||
|
|
7c37f929a5 | ||
|
|
1b82d10b39 | ||
|
|
6b6e2490e7 | ||
|
|
ebee54cf92 | ||
|
|
b7acfb0dcc | ||
|
|
65dab45582 | ||
|
|
43086f9582 | ||
|
|
ed1aa74aff | ||
|
|
3ba128793a | ||
|
|
ffbbdc1576 | ||
|
|
6ff55cfff7 | ||
|
|
e9f1f781e1 | ||
|
|
6da36fe098 | ||
|
|
acb6510312 | ||
|
|
13248506c5 | ||
|
|
fb9740f5c3 | ||
|
|
61bf788f52 | ||
|
|
fd116e0178 | ||
|
|
661d981231 | ||
|
|
6a701591af | ||
|
|
02431c6ef4 | ||
|
|
802b179880 | ||
|
|
6c2104b84b | ||
|
|
a3f432dc88 | ||
|
|
7f4caedf40 | ||
|
|
eebd06f0d8 | ||
|
|
0ea66b6bb0 | ||
|
|
084cdd7200 | ||
|
|
2eff9e0230 | ||
|
|
6615bc4a2a | ||
|
|
387f18be98 | ||
|
|
3b5a3eccfe | ||
|
|
36083a8bd9 | ||
|
|
7e16825bf4 | ||
|
|
9ba2724d0a | ||
|
|
7866851f5d | ||
|
|
c938035ec1 | ||
|
|
965fdc5e9b | ||
|
|
eab932b4a0 | ||
|
|
27a24262c8 | ||
|
|
31584de225 | ||
|
|
6b2f90019a | ||
|
|
208147db9e | ||
|
|
e4f70fa4fe | ||
|
|
4d09abd0d3 | ||
|
|
1304f4dc39 | ||
|
|
ac027d9267 | ||
|
|
0b6d343616 | ||
|
|
92e8f125f9 | ||
|
|
bef15482af | ||
|
|
93d78b3b2e | ||
|
|
d38b7deeeb | ||
|
|
b656e1dd0a | ||
|
|
f1cec895b9 | ||
|
|
9bf6922d97 | ||
|
|
41fc4096e4 | ||
|
|
e46564cb7e | ||
|
|
77751c1d28 | ||
|
|
054a1e4017 | ||
|
|
da27d74111 | ||
|
|
970278228d | ||
|
|
ee3f2d62cf | ||
|
|
ec6d5031cf | ||
|
|
80f338c3af | ||
|
|
268c9a1c26 | ||
|
|
e8e01b5965 | ||
|
|
23a042b667 | ||
|
|
c2c1537858 | ||
|
|
65d5f4c426 | ||
|
|
a1eb33b1f6 | ||
|
|
9eadd92d05 | ||
|
|
b0e1294584 | ||
|
|
f1fd29a477 | ||
|
|
fc9a6b98d1 | ||
|
|
305f6c610c | ||
|
|
66f4732db5 | ||
|
|
ccdfa546b4 | ||
|
|
855e194baa | ||
|
|
e0c06615fb | ||
|
|
03a4809866 | ||
|
|
0aa7bd6a4e | ||
|
|
78b530f8b8 | ||
|
|
ace47c61b1 | ||
|
|
29796f51d7 | ||
|
|
4d2ce7a2be | ||
|
|
8dc45263cd | ||
|
|
d8d95d8efe | ||
|
|
f081591354 | ||
|
|
35e96fecdb | ||
|
|
841fbfa7ee | ||
|
|
89397ae7cc | ||
|
|
6c57c2ac2a | ||
|
|
40663eb52f | ||
|
|
90c9cc17b9 | ||
|
|
125c4f43cf | ||
|
|
634e4abcc1 | ||
|
|
a5431330d1 | ||
|
|
30fc6d94c5 | ||
|
|
694d8f1984 | ||
|
|
bff8fc8230 | ||
|
|
5f7414e84c | ||
|
|
8c707555f2 | ||
|
|
63ce2de3bf | ||
|
|
5e86cca277 | ||
|
|
4bbed2601c | ||
|
|
e0bda8cf53 | ||
|
|
a1c807d65b | ||
|
|
77f5c290cc | ||
|
|
4294b446f3 | ||
|
|
664e8e5526 | ||
|
|
264ade3db9 | ||
|
|
1a6e5e9e2b | ||
|
|
6aa4bb549a | ||
|
|
dd76909f02 | ||
|
|
a454adece8 | ||
|
|
f2adc4d283 | ||
|
|
6af9835f74 | ||
|
|
b823baa387 | ||
|
|
6791d2d46e | ||
|
|
3c343af562 | ||
|
|
fe9ed4c5f7 | ||
|
|
7374e7ee23 | ||
|
|
3c9c0e244a | ||
|
|
64c219b02d | ||
|
|
7e68f8faf2 | ||
|
|
c868098042 | ||
|
|
b3c0cda2be | ||
|
|
2176d1f3df | ||
|
|
979b9859af | ||
|
|
a9ce4a1aed | ||
|
|
a01fb7ff1c | ||
|
|
0e631508b2 | ||
|
|
eb9915d445 | ||
|
|
eedf7d4280 | ||
|
|
aaca487b8f | ||
|
|
a7d6c0f25c | ||
|
|
158a250357 | ||
|
|
b9d7d19dea | ||
|
|
a837f86999 | ||
|
|
a0e4b1aaf9 | ||
|
|
4d10be2aa5 | ||
|
|
8c74ae2fec | ||
|
|
7773341546 | ||
|
|
50747aa0d0 | ||
|
|
353434e1ef | ||
|
|
d70213e031 | ||
|
|
763e14f25f | ||
|
|
806e81743c | ||
|
|
b0b2b02a49 | ||
|
|
5acf442279 | ||
|
|
324e83489e | ||
|
|
8505530547 | ||
|
|
43565d3414 | ||
|
|
9cb8fc8ef5 | ||
|
|
a11c34d1f6 | ||
|
|
03ef778dee | ||
|
|
fa61fa301c | ||
|
|
b4f6177e87 | ||
|
|
f5c16bf824 | ||
|
|
c911dfa9f2 | ||
|
|
297eb55c61 | ||
|
|
4fce7cc3cc | ||
|
|
1d793de213 | ||
|
|
211d79d14d | ||
|
|
f14bce9849 | ||
|
|
8baf07a11c | ||
|
|
a917dace6e | ||
|
|
b3fef6c31e | ||
|
|
05b4fea3fc | ||
|
|
ab1ad28377 | ||
|
|
5beb90dc19 | ||
|
|
9e2e345a3e | ||
|
|
70f774dce9 | ||
|
|
203b16e5a9 | ||
|
|
b3974d6e64 | ||
|
|
dc153ff4e6 | ||
|
|
5ddd7cdb9e | ||
|
|
85787ba1df | ||
|
|
560b2f7d6f | ||
|
|
8260be4bff | ||
|
|
65e0fae3f4 | ||
|
|
e32b81dc2a | ||
|
|
10bcadb26a | ||
|
|
93228f4be7 | ||
|
|
e6a4067a35 | ||
|
|
baf9cd0909 | ||
|
|
9081230286 | ||
|
|
6db71f4a39 | ||
|
|
d628921e48 | ||
|
|
240f27fbad | ||
|
|
23a3d78deb | ||
|
|
47ebcc0f05 | ||
|
|
3264a0a795 | ||
|
|
deeaf2ba2e | ||
|
|
646f79be7d | ||
|
|
07f6baf7c1 | ||
|
|
4f06a0d27c | ||
|
|
9d17bf473c | ||
|
|
279ad7945e | ||
|
|
d079f85eca | ||
|
|
bd8060e533 | ||
|
|
0b21481539 | ||
|
|
3090a8521c | ||
|
|
5959545ae9 | ||
|
|
e7f8d36199 | ||
|
|
09cf8074aa | ||
|
|
06f19aa6cd | ||
|
|
7a4f522144 | ||
|
|
a2985830c5 | ||
|
|
358da730cd | ||
|
|
2c25a0494b | ||
|
|
16b384925e | ||
|
|
fd03cc6319 | ||
|
|
a9fc5c6331 | ||
|
|
dad9d0b708 | ||
|
|
e2ade166ec | ||
|
|
e33a68b203 | ||
|
|
ad52062195 | ||
|
|
236e0faace | ||
|
|
7b4d2661ad | ||
|
|
03b5170b97 | ||
|
|
ea6d1a9381 | ||
|
|
aff5c2aa16 | ||
|
|
829abf169a | ||
|
|
56e008ea4f | ||
|
|
2a16d8baed | ||
|
|
ee89629738 | ||
|
|
3e63ac46b4 | ||
|
|
c881c67f5e | ||
|
|
a3574292c6 | ||
|
|
94b308cecb | ||
|
|
d3e83b12d9 | ||
|
|
d77555266b | ||
|
|
3451ac4504 | ||
|
|
b51973f1d5 | ||
|
|
f568002e5c | ||
|
|
321cced323 | ||
|
|
4359336fd5 | ||
|
|
e8570c3680 | ||
|
|
fd1ff5e438 | ||
|
|
026d029614 | ||
|
|
ef058a1644 | ||
|
|
a35a167e7a | ||
|
|
36ef36be61 | ||
|
|
a874efee4e | ||
|
|
07c32e2a35 | ||
|
|
055f4b09ee | ||
|
|
737b1c962a | ||
|
|
99ac2cb333 | ||
|
|
a183057b32 | ||
|
|
2883c16560 | ||
|
|
d88534e71f | ||
|
|
a56e9e502e | ||
|
|
433e8266c9 | ||
|
|
490feb358c | ||
|
|
27e3c883c3 | ||
|
|
71e2b8225a | ||
|
|
6d4906dfa8 | ||
|
|
0156e74f5a | ||
|
|
c834cb6ff7 | ||
|
|
48360d08d4 | ||
|
|
74877b839e | ||
|
|
39e04bef17 | ||
|
|
c5af204de3 | ||
|
|
ca0dd03042 | ||
|
|
f8f70ed3e1 | ||
|
|
0510588a09 | ||
|
|
1d8fc4b7fd | ||
|
|
b7f5333b39 | ||
|
|
544121d035 | ||
|
|
4da4de3b99 | ||
|
|
0b62c0346b | ||
|
|
292956b18c | ||
|
|
925f347050 | ||
|
|
ea150939cb | ||
|
|
57f490e5db | ||
|
|
a141fdaf7d | ||
|
|
16f1fbf583 | ||
|
|
9d4e13cd08 | ||
|
|
73a7063867 | ||
|
|
cd5c253a78 | ||
|
|
372c6f6ba3 | ||
|
|
d6f4a89326 | ||
|
|
60905c7409 | ||
|
|
f3490d07bf | ||
|
|
b99855afbe | ||
|
|
921c903190 | ||
|
|
9771b53c79 | ||
|
|
0ab5bbb240 | ||
|
|
fef533f101 | ||
|
|
3399af5a96 | ||
|
|
022195508a | ||
|
|
d3daaff6a4 | ||
|
|
89a3c62637 | ||
|
|
6da9db6d86 | ||
|
|
c254b08e33 | ||
|
|
9d575650d1 | ||
|
|
d8ac5a390a | ||
|
|
60842a10ff | ||
|
|
1cab6f87a0 | ||
|
|
a0aeac767d | ||
|
|
99bd8e82ca | ||
|
|
bbdf54097e | ||
|
|
2a9576baf5 | ||
|
|
2ca4c2d1c1 | ||
|
|
3231f8302c | ||
|
|
e0be9b4ef5 | ||
|
|
83b0963533 | ||
|
|
f9548dcffe | ||
|
|
e94a84d4ec | ||
|
|
db5f8707ec | ||
|
|
0f15562a28 | ||
|
|
b300f223ba | ||
|
|
ad9337021c | ||
|
|
5e94c350ed | ||
|
|
666020c3dc | ||
|
|
f249a6edd5 | ||
|
|
2e45bd719a | ||
|
|
28f27915c5 | ||
|
|
08ebca501b | ||
|
|
417cda1d38 | ||
|
|
dd730f5fbf | ||
|
|
77bb3702a9 | ||
|
|
5046f58c6f | ||
|
|
d02f605874 |
@@ -1,5 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*.kt]
|
||||
[*.{kt,kts}]
|
||||
indent_size = 2
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = false
|
||||
ij_kotlin_allow_trailing_comma = false
|
||||
ktlint_code_style = intellij_idea
|
||||
twitter_compose_allowed_composition_locals=LocalExtendedColors
|
||||
4
.github/workflows/android.yml
vendored
@@ -19,11 +19,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: set up JDK 11
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
|
||||
82
.github/workflows/diffuse.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
name: APK Diff
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read # to fetch code (actions/checkout)
|
||||
pull-requests: write # to comment on PR
|
||||
|
||||
jobs:
|
||||
assemble-base:
|
||||
if: ${{ github.repository != 'signalapp/Signal-Android' }}
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
- name: Cache base apk
|
||||
id: cache-base
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: diffuse-base.apk
|
||||
key: diffuse-${{ github.event.pull_request.base.sha }}
|
||||
|
||||
|
||||
- name: Build with Gradle
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
run: ./gradlew assemblePlayProdRelease --parallel
|
||||
|
||||
- name: Copy base apk
|
||||
if: steps.cache-base.outputs.cache-hit != 'true'
|
||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-base.apk
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
clean: 'false'
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assemblePlayProdRelease --parallel
|
||||
|
||||
- name: Copy PR apk
|
||||
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk
|
||||
|
||||
- id: diffuse
|
||||
uses: usefulness/diffuse-action@v1
|
||||
with:
|
||||
old-file-path: diffuse-base.apk
|
||||
new-file-path: diffuse-new.apk
|
||||
|
||||
- uses: peter-evans/find-comment@v2
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
body-includes: Diffuse output
|
||||
|
||||
- uses: peter-evans/create-or-update-comment@v3
|
||||
with:
|
||||
body: |
|
||||
Diffuse output:
|
||||
|
||||
${{ steps.diffuse.outputs.diff-gh-comment }}
|
||||
edit-mode: replace
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: diffuse-output
|
||||
path: ${{ steps.diffuse.outputs.diff-file }}
|
||||
1
.gitignore
vendored
@@ -28,3 +28,4 @@ jni/libspeex/.deps/
|
||||
pkcs11.password
|
||||
dev.keystore
|
||||
maps.key
|
||||
local/
|
||||
14
.idea/codeStyles/Project.xml
generated
@@ -41,13 +41,6 @@
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value />
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
@@ -219,5 +212,12 @@
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
6
.idea/copyright/Signal.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Copyright &#36;today.year Signal Messenger, LLC SPDX-License-Identifier: AGPL-3.0-only" />
|
||||
<option name="myName" value="Signal" />
|
||||
</copyright>
|
||||
</component>
|
||||
7
.idea/copyright/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings>
|
||||
<module2copyright>
|
||||
<element module="All" copyright="Signal" />
|
||||
</module2copyright>
|
||||
</settings>
|
||||
</component>
|
||||
9
.idea/fileTemplates/internal/AnnotationType.java
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public @interface ${NAME} {
|
||||
}
|
||||
9
.idea/fileTemplates/internal/Class.java
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public class ${NAME} {
|
||||
}
|
||||
9
.idea/fileTemplates/internal/Enum.java
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public enum ${NAME} {
|
||||
}
|
||||
9
.idea/fileTemplates/internal/Interface.java
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
|
||||
#parse("File Header.java")
|
||||
public interface ${NAME} {
|
||||
}
|
||||
11
.idea/fileTemplates/internal/Kotlin Class.kt
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
class ${NAME} {
|
||||
}
|
||||
11
.idea/fileTemplates/internal/Kotlin Enum.kt
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
enum class ${NAME} {
|
||||
}
|
||||
9
.idea/fileTemplates/internal/Kotlin File.kt
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
11
.idea/fileTemplates/internal/Kotlin Interface.kt
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright ${YEAR} Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
|
||||
|
||||
#end
|
||||
#parse("File Header.java")
|
||||
interface ${NAME} {
|
||||
}
|
||||
152
LICENSE
@@ -1,23 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -619,3 +617,45 @@ Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -54,8 +54,8 @@ The form and manner of this distribution makes it eligible for export under the
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2013-2022 Signal
|
||||
Copyright 2013-2023 Signal
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
|
||||
|
||||
Google Play and the Google Play logo are trademarks of Google LLC.
|
||||
|
||||
@@ -42,12 +42,11 @@ wire {
|
||||
}
|
||||
|
||||
ktlint {
|
||||
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
|
||||
version = "0.47.1"
|
||||
version = "0.49.1"
|
||||
}
|
||||
|
||||
def canonicalVersionCode = 1237
|
||||
def canonicalVersionName = "6.16.1"
|
||||
def canonicalVersionCode = 1295
|
||||
def canonicalVersionName = "6.27.1"
|
||||
|
||||
def postFixSize = 100
|
||||
def abiPostFix = ['universal' : 0,
|
||||
@@ -67,11 +66,13 @@ def selectableVariants = [
|
||||
'nightlyPnpRelease',
|
||||
'playProdDebug',
|
||||
'playProdSpinner',
|
||||
'playProdCanary',
|
||||
'playProdPerf',
|
||||
'playProdBenchmark',
|
||||
'playProdInstrumentation',
|
||||
'playProdRelease',
|
||||
'playStagingDebug',
|
||||
'playStagingCanary',
|
||||
'playStagingSpinner',
|
||||
'playStagingPerf',
|
||||
'playStagingInstrumentation',
|
||||
@@ -157,7 +158,7 @@ android {
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = '1.3.2'
|
||||
kotlinCompilerExtensionVersion = '1.4.4'
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -180,10 +181,10 @@ android {
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_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\"}"
|
||||
@@ -194,12 +195,14 @@ android {
|
||||
buildConfigField "String[]", "SIGNAL_STORAGE_IPS", storage_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDN_IPS", cdn_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDN2_IPS", cdn2_ips
|
||||
buildConfigField "String[]", "SIGNAL_CDS_IPS", cds_ips
|
||||
buildConfigField "String[]", "SIGNAL_KBS_IPS", kbs_ips
|
||||
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
|
||||
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
|
||||
buildConfigField "String[]", "SIGNAL_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\")"
|
||||
@@ -208,6 +211,7 @@ android {
|
||||
"\"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\""
|
||||
@@ -225,8 +229,8 @@ android {
|
||||
ndk {
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
}
|
||||
resourceConfigurations += []
|
||||
|
||||
resConfigs autoResConfig()
|
||||
|
||||
splits {
|
||||
abi {
|
||||
@@ -318,6 +322,14 @@ android {
|
||||
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 {
|
||||
@@ -365,9 +377,10 @@ android {
|
||||
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
|
||||
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
|
||||
buildConfigField "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\", " +
|
||||
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
|
||||
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
|
||||
@@ -376,6 +389,7 @@ android {
|
||||
"\"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\""
|
||||
@@ -434,10 +448,17 @@ android {
|
||||
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.core.ktx
|
||||
implementation libs.androidx.fragment.ktx
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
@@ -457,6 +478,8 @@ dependencies {
|
||||
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
|
||||
@@ -514,23 +537,15 @@ dependencies {
|
||||
exclude group: 'com.google.protobuf'
|
||||
}
|
||||
|
||||
implementation(libs.signal.argon2) {
|
||||
artifact {
|
||||
type = "aar"
|
||||
}
|
||||
}
|
||||
|
||||
implementation libs.signal.ringrtc
|
||||
|
||||
implementation libs.leolin.shortcutbadger
|
||||
implementation libs.emilsjolander.stickylistheaders
|
||||
implementation libs.jpardogo.materialtabstrip
|
||||
implementation libs.apache.httpclient.android
|
||||
implementation libs.glide.glide
|
||||
implementation libs.roundedimageview
|
||||
implementation libs.materialish.progress
|
||||
implementation libs.greenrobot.eventbus
|
||||
implementation libs.waitingdots
|
||||
implementation libs.google.zxing.android.integration
|
||||
implementation libs.google.zxing.core
|
||||
implementation libs.google.flexbox
|
||||
@@ -556,9 +571,12 @@ dependencies {
|
||||
exclude group: 'org.freemarker'
|
||||
}
|
||||
implementation libs.dnsjava
|
||||
implementation libs.kotlinx.collections.immutable
|
||||
implementation libs.accompanist.permissions
|
||||
|
||||
spinnerImplementation project(":spinner")
|
||||
spinnerImplementation libs.square.leakcanary
|
||||
|
||||
canaryImplementation libs.square.leakcanary
|
||||
|
||||
testImplementation testLibs.junit.junit
|
||||
testImplementation testLibs.assertj.core
|
||||
@@ -570,9 +588,8 @@ dependencies {
|
||||
exclude group: 'com.google.protobuf', module: 'protobuf-java'
|
||||
}
|
||||
testImplementation testLibs.robolectric.shadows.multidex
|
||||
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) {
|
||||
force = true
|
||||
}
|
||||
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.hamcrest.hamcrest
|
||||
testImplementation testLibs.mockk
|
||||
|
||||
|
||||
@@ -11,4 +11,11 @@
|
||||
# Protobuf lite
|
||||
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
|
||||
|
||||
-keep class androidx.window.** { *; }
|
||||
-keep class androidx.window.** { *; }
|
||||
|
||||
# AGP generated dont warns
|
||||
-dontwarn com.android.org.conscrypt.SSLParametersImpl
|
||||
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
-dontwarn sun.net.spi.nameservice.NameService
|
||||
-dontwarn sun.net.spi.nameservice.NameServiceDescriptor
|
||||
@@ -10,13 +10,11 @@ import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.mock
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
|
||||
@@ -48,20 +46,17 @@ class ChangeNumberViewModelTest {
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var viewModel: ChangeNumberViewModel
|
||||
private lateinit var kbsRepository: KbsRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
ApplicationDependencies.getSignalServiceAccountManager().setSoTimeoutMillis(1000)
|
||||
kbsRepository = mock()
|
||||
ThreadUtil.runOnMainSync {
|
||||
viewModel = ChangeNumberViewModel(
|
||||
localNumber = harness.self.requireE164(),
|
||||
changeNumberRepository = ChangeNumberRepository(),
|
||||
savedState = SavedStateHandle(),
|
||||
password = SignalStore.account().servicePassword!!,
|
||||
verifyAccountRepository = VerifyAccountRepository(harness.application),
|
||||
kbsRepository = kbsRepository
|
||||
verifyAccountRepository = VerifyAccountRepository(harness.application)
|
||||
)
|
||||
|
||||
viewModel.setNewCountry(1)
|
||||
@@ -235,7 +230,7 @@ class ChangeNumberViewModelTest {
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
|
||||
MockProvider.mockGetRegistrationLockStringFlow()
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
@@ -323,7 +318,7 @@ class ChangeNumberViewModelTest {
|
||||
lateinit var changeNumberRequest: ChangePhoneNumberRequest
|
||||
lateinit var setPreKeysRequest: PreKeyState
|
||||
|
||||
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
|
||||
MockProvider.mockGetRegistrationLockStringFlow()
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
|
||||
|
||||
@@ -142,6 +142,7 @@ class ConversationItemPreviewer {
|
||||
1024,
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of("/not-there.jpg"),
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.conversation.v2.items
|
||||
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.lifecycle.Observer
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.database.FakeMessageRecords
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
class V2ConversationItemShapeTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(othersCount = 10)
|
||||
|
||||
@Test
|
||||
fun givenNextAndPreviousMessageDoNotExist_whenISetMessageShape_thenIExpectSingle() {
|
||||
val testSubject = V2ConversationItemShape(FakeConversationContext())
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreviousWithinTimeoutAndNoNext_whenISetMessageShape_thenIExpectEnd() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 2.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
previousMessage = getMessageRecord(prev)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.END
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNextWithinTimeoutAndNoPrevious_whenISetMessageShape_thenIExpectStart() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 2.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
nextMessage = getMessageRecord(now)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.START
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(prev),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreviousAndNextWithinTimeout_whenISetMessageShape_thenIExpectMiddle() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 2.minutes.inWholeMilliseconds
|
||||
val next = now + 2.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
previousMessage = getMessageRecord(prev),
|
||||
nextMessage = getMessageRecord(next)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.MIDDLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreviousOutsideTimeoutAndNoNext_whenISetMessageShape_thenIExpectSingle() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 4.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
previousMessage = getMessageRecord(prev)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNextOutsideTimeoutAndNoPrevious_whenISetMessageShape_thenIExpectSingle() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 4.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
nextMessage = getMessageRecord(now)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(prev),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreviousAndNextOutsideTimeout_whenISetMessageShape_thenIExpectSingle() {
|
||||
val now = System.currentTimeMillis()
|
||||
val prev = now - 4.minutes.inWholeMilliseconds
|
||||
val next = now + 4.minutes.inWholeMilliseconds
|
||||
|
||||
val testSubject = V2ConversationItemShape(
|
||||
FakeConversationContext(
|
||||
previousMessage = getMessageRecord(prev),
|
||||
nextMessage = getMessageRecord(next)
|
||||
)
|
||||
)
|
||||
|
||||
val expected = V2ConversationItemShape.MessageShape.SINGLE
|
||||
val actual = testSubject.setMessageShape(
|
||||
isLtr = true,
|
||||
currentMessage = getMessageRecord(now),
|
||||
isGroupThread = false,
|
||||
adapterPosition = 5
|
||||
)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
private fun getMessageRecord(
|
||||
timestamp: Long = System.currentTimeMillis()
|
||||
): MessageRecord {
|
||||
return FakeMessageRecords.buildMediaMmsMessageRecord(
|
||||
dateReceived = timestamp,
|
||||
dateSent = timestamp,
|
||||
dateServer = timestamp
|
||||
)
|
||||
}
|
||||
|
||||
private class FakeConversationContext(
|
||||
private val hasWallpaper: Boolean = false,
|
||||
private val previousMessage: MessageRecord? = null,
|
||||
private val nextMessage: MessageRecord? = null
|
||||
) : V2ConversationContext {
|
||||
|
||||
private val colorizer = Colorizer()
|
||||
|
||||
override val 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 fun onStartExpirationTimeout(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun hasWallpaper(): Boolean = hasWallpaper
|
||||
|
||||
override fun getColorizer(): Colorizer = colorizer
|
||||
|
||||
override fun getNextMessage(adapterPosition: Int): MessageRecord? = nextMessage
|
||||
|
||||
override fun getPreviousMessage(adapterPosition: Int): MessageRecord? = previousMessage
|
||||
}
|
||||
|
||||
private object FakeConversationItemClickListener : ConversationAdapter.ItemClickListener {
|
||||
override fun onQuoteClicked(messageRecord: MmsMessageRecord?) = Unit
|
||||
|
||||
override fun onLinkPreviewClicked(linkPreview: LinkPreview) = Unit
|
||||
|
||||
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) = Unit
|
||||
|
||||
override fun onStickerClicked(stickerLocator: StickerLocator) = Unit
|
||||
|
||||
override fun onViewOnceMessageClicked(messageRecord: MmsMessageRecord) = Unit
|
||||
|
||||
override fun onSharedContactDetailsClicked(contact: Contact, avatarTransitionView: View) = Unit
|
||||
|
||||
override fun onAddToContactsClicked(contact: Contact) = Unit
|
||||
|
||||
override fun onMessageSharedContactClicked(choices: MutableList<Recipient>) = Unit
|
||||
|
||||
override fun onInviteSharedContactClicked(choices: MutableList<Recipient>) = Unit
|
||||
|
||||
override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) = Unit
|
||||
|
||||
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) = Unit
|
||||
|
||||
override fun onMessageWithErrorClicked(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun onIncomingIdentityMismatchClicked(recipientId: RecipientId) = Unit
|
||||
|
||||
override fun onRegisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) = Unit
|
||||
|
||||
override fun onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) = Unit
|
||||
|
||||
override fun onVoiceNotePause(uri: Uri) = Unit
|
||||
|
||||
override fun onVoiceNotePlay(uri: Uri, messageId: Long, position: Double) = Unit
|
||||
|
||||
override fun onVoiceNoteSeekTo(uri: Uri, position: Double) = Unit
|
||||
|
||||
override fun onVoiceNotePlaybackSpeedChanged(uri: Uri, speed: Float) = Unit
|
||||
|
||||
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) = Unit
|
||||
|
||||
override fun onChatSessionRefreshLearnMoreClicked() = Unit
|
||||
|
||||
override fun onBadDecryptLearnMoreClicked(author: RecipientId) = Unit
|
||||
|
||||
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) = Unit
|
||||
|
||||
override fun onJoinGroupCallClicked() = Unit
|
||||
|
||||
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) = Unit
|
||||
|
||||
override fun onEnableCallNotificationsClicked() = Unit
|
||||
|
||||
override fun onPlayInlineContent(conversationMessage: ConversationMessage?) = Unit
|
||||
|
||||
override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) = Unit
|
||||
|
||||
override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) = Unit
|
||||
|
||||
override fun onChangeNumberUpdateContact(recipient: Recipient) = Unit
|
||||
|
||||
override fun onCallToAction(action: String) = Unit
|
||||
|
||||
override fun onDonateClicked() = Unit
|
||||
|
||||
override fun onBlockJoinRequest(recipient: Recipient) = Unit
|
||||
|
||||
override fun onRecipientNameClicked(target: RecipientId) = Unit
|
||||
|
||||
override fun onInviteToSignalClicked() = Unit
|
||||
|
||||
override fun onActivatePaymentsClicked() = Unit
|
||||
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) = Unit
|
||||
|
||||
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) = Unit
|
||||
|
||||
override fun onUrlClicked(url: String): Boolean = false
|
||||
|
||||
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun onGiftBadgeRevealed(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit
|
||||
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
|
||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
|
||||
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) = Unit
|
||||
|
||||
override fun onItemClick(item: MultiselectPart?) = Unit
|
||||
|
||||
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) = Unit
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import androidx.test.filters.FlakyTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.attachments.UriAttachment
|
||||
@@ -64,6 +65,7 @@ class AttachmentTableTest {
|
||||
}
|
||||
|
||||
@FlakyTest
|
||||
@Ignore("test is flaky")
|
||||
@Test
|
||||
fun givenIdenticalAttachmentsInsertedForPreUpload_whenIUpdateAttachmentDataAndSpecifyOnlyModifyThisAttachment_thenIExpectDifferentFileInfos() {
|
||||
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkState
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CallLinkTableTest {
|
||||
|
||||
companion object {
|
||||
private val ROOM_ID_A = byteArrayOf(1, 2, 3, 4)
|
||||
private val ROOM_ID_B = byteArrayOf(2, 2, 3, 4)
|
||||
private const val TIMESTAMP_A = 1000L
|
||||
private const val TIMESTAMP_B = 2000L
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteBeforeFirst_thenIExpectNeitherDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_A - 500)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(2, callEvents.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteOnFirst_thenIExpectFirstDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_A)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(1, callEvents.size)
|
||||
assertEquals(TIMESTAMP_B, callEvents.first().record.timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteAfterFirstAndBeforeSecond_thenIExpectFirstDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_B - 500)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(1, callEvents.size)
|
||||
assertEquals(TIMESTAMP_B, callEvents.first().record.timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteOnSecond_thenIExpectBothDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_B)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(0, callEvents.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoNonAdminCallLinks_whenIDeleteAfterSecond_thenIExpectBothDeleted() {
|
||||
insertTwoNonAdminCallLinksWithEvents()
|
||||
SignalDatabase.callLinks.deleteNonAdminCallLinksOnOrBefore(TIMESTAMP_B + 500)
|
||||
val callEvents = SignalDatabase.calls.getCalls(0, 2, "", CallLogFilter.ALL)
|
||||
assertEquals(0, callEvents.size)
|
||||
}
|
||||
|
||||
private fun insertTwoNonAdminCallLinksWithEvents() {
|
||||
insertCallLinkWithEvent(ROOM_ID_A, 1000)
|
||||
insertCallLinkWithEvent(ROOM_ID_B, 2000)
|
||||
}
|
||||
|
||||
private fun insertCallLinkWithEvent(roomId: ByteArray, timestamp: Long) {
|
||||
SignalDatabase.callLinks.insertCallLink(
|
||||
CallLinkTable.CallLink(
|
||||
recipientId = RecipientId.UNKNOWN,
|
||||
roomId = CallLinkRoomId.fromBytes(roomId),
|
||||
credentials = CallLinkCredentials(
|
||||
linkKeyBytes = roomId,
|
||||
adminPassBytes = null
|
||||
),
|
||||
state = SignalCallLinkState()
|
||||
)
|
||||
)
|
||||
|
||||
val callLinkRecipient = SignalDatabase.recipients.getByCallLinkRoomId(CallLinkRoomId.fromBytes(roomId)).get()
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
1,
|
||||
callLinkRecipient,
|
||||
CallTable.Direction.INCOMING,
|
||||
timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,825 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.ringrtc.CallId
|
||||
import org.signal.ringrtc.CallManager
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CallTableTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(createGroup = true)
|
||||
|
||||
private val groupRecipientId: RecipientId
|
||||
get() = harness.group!!.recipientId
|
||||
|
||||
@Test
|
||||
fun givenACall_whenISetTimestamp_thenIExpectUpdatedTimestamp() {
|
||||
val callId = 1L
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.setTimestamp(callId, groupRecipientId, -1L)
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(-1L, call?.timestamp)
|
||||
|
||||
val messageRecord = SignalDatabase.messages.getMessageRecord(call!!.messageId!!)
|
||||
assertEquals(-1L, messageRecord.dateReceived)
|
||||
assertEquals(-1L, messageRecord.dateSent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenPreExistingEvent_whenIDeleteGroupCall_thenIMarkDeletedAndSetTimestamp() {
|
||||
val callId = 1L
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
SignalDatabase.calls.deleteGroupCall(call!!)
|
||||
|
||||
val deletedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
|
||||
|
||||
assertEquals(CallTable.Event.DELETE, deletedCall?.event)
|
||||
assertNotEquals(0L, oldestDeletionTimestamp)
|
||||
assertNull(deletedCall!!.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
|
||||
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
|
||||
|
||||
assertEquals(CallTable.Event.DELETE, call?.event)
|
||||
assertNotEquals(oldestDeletionTimestamp, 0)
|
||||
assertNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenIInsertAcceptedOutgoingGroupCall_thenIExpectLocalRingerAndOutgoingRing() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
|
||||
assertEquals(harness.self.id, call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenIInsertAcceptedIncomingGroupCall_thenIExpectJoined() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.JOINED, call?.event)
|
||||
assertNull(call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCall_whenIAcceptedIncomingGroupCall_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!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAMissedCall_whenIAcceptedIncomingGroupCall_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.acceptIncomingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADeclinedCall_whenIAcceptedIncomingGroupCall_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.acceptIncomingGroupCall(
|
||||
call!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGenericGroupCall_whenIAcceptedIncomingGroupCall_thenIExpectAccepted() {
|
||||
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!!
|
||||
)
|
||||
|
||||
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertEquals(CallTable.Event.JOINED, acceptedCall?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = System.currentTimeMillis(),
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAPriorCallEventWithNewerTimestamp_whenIReceiveAGroupCallUpdateMessage_thenIExpectAnUpdatedTimestamp() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.getCallById(callId, groupRecipientId).let {
|
||||
assertNotNull(it)
|
||||
assertEquals(now, it?.timestamp)
|
||||
}
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = 1L,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
|
||||
assertEquals(1L, call?.timestamp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
|
||||
callId = callId,
|
||||
recipientId = groupRecipientId,
|
||||
direction = CallTable.Direction.INCOMING,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
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.DELETE, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGenericCallEvent_whenRingRequested_thenISetRingerAndMoveToRingingState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAJoinedCallEvent_whenRingRequested_thenISetRingerAndMoveToRingingState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGenericCallEvent_whenRingExpired_thenISetRingerAndMoveToMissedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCallEvent_whenRingExpired_thenISetRingerAndMoveToMissedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAJoinedCallEvent_whenRingIsCancelledBecauseUserIsBusyLocally_thenIMoveToAcceptedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_LOCALLY
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAJoinedCallEvent_whenRingIsCancelledBecauseUserIsBusyOnAnotherDevice_thenIMoveToAcceptedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
now
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCallEvent_whenRingCancelledBecauseUserIsBusyLocally_thenIMoveToMissedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_LOCALLY
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCallEvent_whenRingCancelledBecauseUserIsBusyOnAnotherDevice_thenIMoveToMissedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenACallEvent_whenRingIsAcceptedOnAnotherDevice_thenIMoveToAcceptedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
val now = System.currentTimeMillis()
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
|
||||
groupRecipientId = groupRecipientId,
|
||||
sender = harness.others[1],
|
||||
timestamp = now,
|
||||
peekGroupCallEraId = "aaa",
|
||||
peekJoinedUuids = emptyList(),
|
||||
isCallFull = false
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenARingingCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAMissedCallEvent_whenRingDeclinedOnAnotherDevice_thenIMoveToDeclinedState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAnOutgoingRingCallEvent_whenRingDeclinedOnAnotherDevice_thenIDoNotChangeState() {
|
||||
val era = "aaa"
|
||||
val callId = CallId.fromEra(era).longValue()
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.OUTGOING,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingRequested_thenICreateAnEventInTheRingingStateAndSetRinger() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.REQUESTED
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.RINGING, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingExpired_thenICreateAnEventInTheMissedStateAndSetRinger() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.EXPIRED_REQUEST
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingCancelledByRinger_thenICreateAnEventInTheMissedStateAndSetRinger() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.CANCELLED_BY_RINGER
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertEquals(harness.others[1], call?.ringerRecipient)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyLocally_thenICreateAnEventInTheMissedState() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_LOCALLY
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingCancelledBecauseUserIsBusyOnAnotherDevice_thenICreateAnEventInTheMissedState() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.BUSY_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.MISSED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingAcceptedOnAnotherDevice_thenICreateAnEventInTheAcceptedState() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.ACCEPTED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.ACCEPTED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoPriorEvent_whenRingDeclinedOnAnotherDevice_thenICreateAnEventInTheDeclinedState() {
|
||||
val callId = 1L
|
||||
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
|
||||
callId,
|
||||
groupRecipientId,
|
||||
harness.others[1],
|
||||
System.currentTimeMillis(),
|
||||
CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
|
||||
)
|
||||
|
||||
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
|
||||
assertNotNull(call)
|
||||
assertEquals(CallTable.Event.DECLINED, call?.event)
|
||||
assertNotNull(call?.messageId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteBeforeCallB_thenOnlyDeleteCallA() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(1500)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(1, allCallEvents.size)
|
||||
assertEquals(2, allCallEvents.first().record.callId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteBeforeCallA_thenIDoNotDeleteAnyCalls() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(500)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(2, allCallEvents.size)
|
||||
assertEquals(2, allCallEvents[0].record.callId)
|
||||
assertEquals(1, allCallEvents[1].record.callId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteOnCallA_thenIOnlyDeleteCallA() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(1000)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(1, allCallEvents.size)
|
||||
assertEquals(2, allCallEvents.first().record.callId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteOnCallB_thenIDeleteBothCalls() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(2000)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(0, allCallEvents.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenTwoCalls_whenIDeleteAfterCallB_thenIDeleteBothCalls() {
|
||||
insertTwoCallEvents()
|
||||
|
||||
SignalDatabase.calls.deleteNonAdHocCallEventsOnOrBefore(2500)
|
||||
|
||||
val allCallEvents = SignalDatabase.calls.getCalls(0, 2, null, CallLogFilter.ALL)
|
||||
assertEquals(0, allCallEvents.size)
|
||||
}
|
||||
|
||||
private fun insertTwoCallEvents() {
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
1,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.INCOMING,
|
||||
1000
|
||||
)
|
||||
|
||||
SignalDatabase.calls.insertAcceptedGroupCall(
|
||||
2,
|
||||
groupRecipientId,
|
||||
CallTable.Direction.OUTGOING,
|
||||
2000
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ForeignKeyConstraint
|
||||
import org.signal.core.util.Index
|
||||
import org.signal.core.util.getForeignKeys
|
||||
import org.signal.core.util.getIndexes
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
|
||||
/**
|
||||
* A test that guarantees that a freshly-created database looks the same as one that went through the upgrade path.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DatabaseConsistencyTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun testUpgradeConsistency() {
|
||||
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
|
||||
val testHelper = InMemoryTestHelper(ApplicationDependencies.getApplication()).also {
|
||||
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
|
||||
}
|
||||
|
||||
val upgradedStatements = testHelper.readableDatabase.getAllCreateStatements()
|
||||
|
||||
if (currentVersionStatements != upgradedStatements) {
|
||||
var message = "\n"
|
||||
|
||||
val currentByName = currentVersionStatements.associateBy { it.name }
|
||||
val upgradedByName = upgradedStatements.associateBy { it.name }
|
||||
|
||||
if (currentByName.keys != upgradedByName.keys) {
|
||||
val exclusiveToCurrent = currentByName.keys - upgradedByName.keys
|
||||
val exclusiveToUpgrade = upgradedByName.keys - currentByName.keys
|
||||
|
||||
message += "SQL entities exclusive to the newly-created database: $exclusiveToCurrent\n"
|
||||
message += "SQL entities exclusive to the upgraded database: $exclusiveToUpgrade\n\n"
|
||||
} else {
|
||||
for (currentEntry in currentByName) {
|
||||
val upgradedValue: Statement = upgradedByName[currentEntry.key]!!
|
||||
if (upgradedValue.sql != currentEntry.value.sql) {
|
||||
message += "Statement differed:\n"
|
||||
message += "newly-created:\n"
|
||||
message += "${currentEntry.value.sql}\n\n"
|
||||
message += "upgraded:\n"
|
||||
message += "${upgradedValue.sql}\n\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(message, false)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testForeignKeyIndexCoverage() {
|
||||
/** We may deem certain indexes non-critical if deletion frequency is low or table size is small. */
|
||||
val ignoredColumns: List<Pair<String, String>> = listOf(
|
||||
StorySendTable.TABLE_NAME to StorySendTable.DISTRIBUTION_ID
|
||||
)
|
||||
|
||||
val foreignKeys: List<ForeignKeyConstraint> = SignalDatabase.rawDatabase.getForeignKeys()
|
||||
val indexesByFirstColumn: List<Index> = SignalDatabase.rawDatabase.getIndexes()
|
||||
|
||||
val notFound: List<Pair<String, String>> = foreignKeys
|
||||
.filterNot { ignoredColumns.contains(it.table to it.column) }
|
||||
.filterNot { foreignKey ->
|
||||
indexesByFirstColumn.hasPrimaryIndexFor(foreignKey.table, foreignKey.column)
|
||||
}
|
||||
.map { it.table to it.column }
|
||||
|
||||
assertTrue("Missing indexes to cover: $notFound", notFound.isEmpty())
|
||||
}
|
||||
|
||||
private fun List<Index>.hasPrimaryIndexFor(table: String, column: String): Boolean {
|
||||
return this.any { index -> index.table == table && index.columns[0] == column }
|
||||
}
|
||||
|
||||
private data class Statement(
|
||||
val name: String,
|
||||
val sql: String
|
||||
)
|
||||
|
||||
private fun SQLiteDatabase.getAllCreateStatements(): List<Statement> {
|
||||
return this.rawQuery("SELECT name, sql FROM sqlite_schema WHERE sql NOT NULL AND name != 'sqlite_sequence'")
|
||||
.readToList { cursor ->
|
||||
Statement(
|
||||
name = cursor.requireNonNullString("name"),
|
||||
sql = cursor.requireNonNullString("sql").normalizeSql()
|
||||
)
|
||||
}
|
||||
.filterNot { it.name.startsWith("sqlite_stat") }
|
||||
.sortedBy { it.name }
|
||||
}
|
||||
|
||||
private fun String.normalizeSql(): String {
|
||||
return this
|
||||
.split("\n")
|
||||
.map { it.trim() }
|
||||
.joinToString(separator = " ")
|
||||
.replace(Regex.fromLiteral(" ,"), ",")
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.replace(Regex.fromLiteral("( "), "(")
|
||||
.replace(Regex.fromLiteral(" )"), ")")
|
||||
.replace(Regex("CREATE TABLE \"([a-zA-Z_]+)\""), "CREATE TABLE $1") // for some reason SQLite will wrap table names in quotes for upgraded tables. This unwraps them.
|
||||
}
|
||||
|
||||
private class InMemoryTestHelper(private val application: Application) : SQLiteOpenHelper(application, null, null, 1) {
|
||||
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
for (statement in SNAPSHOT_V181) {
|
||||
db.execSQL(statement.sql)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
SignalDatabaseMigrations.migrate(application, db, 181, SignalDatabaseMigrations.DATABASE_VERSION)
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the list of statements that existed at version 181. Never change this.
|
||||
*/
|
||||
private val SNAPSHOT_V181 = listOf(
|
||||
Statement(
|
||||
name = "message",
|
||||
sql = "CREATE TABLE message (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n date_server INTEGER DEFAULT -1,\n thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n recipient_device_id INTEGER,\n type INTEGER NOT NULL,\n body TEXT,\n read INTEGER DEFAULT 0,\n ct_l TEXT,\n exp INTEGER,\n m_type INTEGER,\n m_size INTEGER,\n st INTEGER,\n tr_id TEXT,\n subscription_id INTEGER DEFAULT -1, \n receipt_timestamp INTEGER DEFAULT -1, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n viewed_receipt_count INTEGER DEFAULT 0,\n mismatched_identities TEXT DEFAULT NULL,\n network_failures TEXT DEFAULT NULL,\n expires_in INTEGER DEFAULT 0,\n expire_started INTEGER DEFAULT 0,\n notified INTEGER DEFAULT 0,\n quote_id INTEGER DEFAULT 0,\n quote_author INTEGER DEFAULT 0,\n quote_body TEXT DEFAULT NULL,\n quote_missing INTEGER DEFAULT 0,\n quote_mentions BLOB DEFAULT NULL,\n quote_type INTEGER DEFAULT 0,\n shared_contacts TEXT DEFAULT NULL,\n unidentified INTEGER DEFAULT 0,\n link_previews TEXT DEFAULT NULL,\n view_once INTEGER DEFAULT 0,\n reactions_unread INTEGER DEFAULT 0,\n reactions_last_seen INTEGER DEFAULT -1,\n remote_deleted INTEGER DEFAULT 0,\n mentions_self INTEGER DEFAULT 0,\n notified_timestamp INTEGER DEFAULT 0,\n server_guid TEXT DEFAULT NULL,\n message_ranges BLOB DEFAULT NULL,\n story_type INTEGER DEFAULT 0,\n parent_story_id INTEGER DEFAULT 0,\n export_state BLOB DEFAULT NULL,\n exported INTEGER DEFAULT 0,\n scheduled_date INTEGER DEFAULT -1\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "part",
|
||||
sql = "CREATE TABLE part (_id INTEGER PRIMARY KEY, mid INTEGER, seq INTEGER DEFAULT 0, ct TEXT, name TEXT, chset INTEGER, cd TEXT, fn TEXT, cid TEXT, cl TEXT, ctt_s INTEGER, ctt_t TEXT, encrypted INTEGER, pending_push INTEGER, _data TEXT, data_size INTEGER, file_name TEXT, unique_id INTEGER NOT NULL, digest BLOB, fast_preflight_id TEXT, voice_note INTEGER DEFAULT 0, borderless INTEGER DEFAULT 0, video_gif INTEGER DEFAULT 0, data_random BLOB, quote INTEGER DEFAULT 0, width INTEGER DEFAULT 0, height INTEGER DEFAULT 0, caption TEXT DEFAULT NULL, sticker_pack_id TEXT DEFAULT NULL, sticker_pack_key DEFAULT NULL, sticker_id INTEGER DEFAULT -1, sticker_emoji STRING DEFAULT NULL, data_hash TEXT DEFAULT NULL, blur_hash TEXT DEFAULT NULL, transform_properties TEXT DEFAULT NULL, transfer_file TEXT DEFAULT NULL, display_order INTEGER DEFAULT 0, upload_timestamp INTEGER DEFAULT 0, cdn_number INTEGER DEFAULT 0)"
|
||||
),
|
||||
Statement(
|
||||
name = "thread",
|
||||
sql = "CREATE TABLE thread (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n date INTEGER DEFAULT 0, \n meaningful_messages INTEGER DEFAULT 0,\n recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE,\n read INTEGER DEFAULT 1, \n type INTEGER DEFAULT 0, \n error INTEGER DEFAULT 0, \n snippet TEXT, \n snippet_type INTEGER DEFAULT 0, \n snippet_uri TEXT DEFAULT NULL, \n snippet_content_type TEXT DEFAULT NULL, \n snippet_extras TEXT DEFAULT NULL, \n unread_count INTEGER DEFAULT 0, \n archived INTEGER DEFAULT 0, \n status INTEGER DEFAULT 0, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n expires_in INTEGER DEFAULT 0, \n last_seen INTEGER DEFAULT 0, \n has_sent INTEGER DEFAULT 0, \n last_scrolled INTEGER DEFAULT 0, \n pinned INTEGER DEFAULT 0, \n unread_self_mention_count INTEGER DEFAULT 0\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "identities",
|
||||
sql = "CREATE TABLE identities (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address INTEGER UNIQUE, \n identity_key TEXT, \n first_use INTEGER DEFAULT 0, \n timestamp INTEGER DEFAULT 0, \n verified INTEGER DEFAULT 0, \n nonblocking_approval INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "drafts",
|
||||
sql = "CREATE TABLE drafts (\n _id INTEGER PRIMARY KEY, \n thread_id INTEGER, \n type TEXT, \n value TEXT\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "push",
|
||||
sql = "CREATE TABLE push (_id INTEGER PRIMARY KEY, type INTEGER, source TEXT, source_uuid TEXT, device_id INTEGER, body TEXT, content TEXT, timestamp INTEGER, server_timestamp INTEGER DEFAULT 0, server_delivered_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL)"
|
||||
),
|
||||
Statement(
|
||||
name = "groups",
|
||||
sql = "CREATE TABLE groups (\n _id INTEGER PRIMARY KEY, \n group_id TEXT, \n recipient_id INTEGER,\n title TEXT,\n avatar_id INTEGER, \n avatar_key BLOB,\n avatar_content_type TEXT, \n avatar_relay TEXT,\n timestamp INTEGER,\n active INTEGER DEFAULT 1,\n avatar_digest BLOB, \n mms INTEGER DEFAULT 0, \n master_key BLOB, \n revision BLOB, \n decrypted_group BLOB, \n expected_v2_id TEXT DEFAULT NULL, \n former_v1_members TEXT DEFAULT NULL, \n distribution_id TEXT DEFAULT NULL, \n display_as_story INTEGER DEFAULT 0, \n auth_service_id TEXT DEFAULT NULL, \n last_force_update_timestamp INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "group_membership",
|
||||
sql = "CREATE TABLE group_membership ( _id INTEGER PRIMARY KEY, group_id TEXT NOT NULL, recipient_id INTEGER NOT NULL, UNIQUE(group_id, recipient_id) )"
|
||||
),
|
||||
Statement(
|
||||
name = "recipient",
|
||||
sql = "CREATE TABLE recipient (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n uuid TEXT UNIQUE DEFAULT NULL,\n username TEXT UNIQUE DEFAULT NULL,\n phone TEXT UNIQUE DEFAULT NULL,\n email TEXT UNIQUE DEFAULT NULL,\n group_id TEXT UNIQUE DEFAULT NULL,\n group_type INTEGER DEFAULT 0,\n blocked INTEGER DEFAULT 0,\n message_ringtone TEXT DEFAULT NULL, \n message_vibrate INTEGER DEFAULT 0, \n call_ringtone TEXT DEFAULT NULL, \n call_vibrate INTEGER DEFAULT 0, \n notification_channel TEXT DEFAULT NULL, \n mute_until INTEGER DEFAULT 0, \n color TEXT DEFAULT NULL, \n seen_invite_reminder INTEGER DEFAULT 0,\n default_subscription_id INTEGER DEFAULT -1,\n message_expiration_time INTEGER DEFAULT 0,\n registered INTEGER DEFAULT 0,\n system_given_name TEXT DEFAULT NULL, \n system_family_name TEXT DEFAULT NULL, \n system_display_name TEXT DEFAULT NULL, \n system_photo_uri TEXT DEFAULT NULL, \n system_phone_label TEXT DEFAULT NULL, \n system_phone_type INTEGER DEFAULT -1, \n system_contact_uri TEXT DEFAULT NULL, \n system_info_pending INTEGER DEFAULT 0, \n profile_key TEXT DEFAULT NULL, \n profile_key_credential TEXT DEFAULT NULL, \n signal_profile_name TEXT DEFAULT NULL, \n profile_family_name TEXT DEFAULT NULL, \n profile_joined_name TEXT DEFAULT NULL, \n signal_profile_avatar TEXT DEFAULT NULL, \n profile_sharing INTEGER DEFAULT 0, \n last_profile_fetch INTEGER DEFAULT 0, \n unidentified_access_mode INTEGER DEFAULT 0, \n force_sms_selection INTEGER DEFAULT 0, \n storage_service_key TEXT UNIQUE DEFAULT NULL, \n mention_setting INTEGER DEFAULT 0, \n storage_proto TEXT DEFAULT NULL,\n capabilities INTEGER DEFAULT 0,\n last_session_reset BLOB DEFAULT NULL,\n wallpaper BLOB DEFAULT NULL,\n wallpaper_file TEXT DEFAULT NULL,\n about TEXT DEFAULT NULL,\n about_emoji TEXT DEFAULT NULL,\n extras BLOB DEFAULT NULL,\n groups_in_common INTEGER DEFAULT 0,\n chat_colors BLOB DEFAULT NULL,\n custom_chat_colors_id INTEGER DEFAULT 0,\n badges BLOB DEFAULT NULL,\n pni TEXT DEFAULT NULL,\n distribution_list_id INTEGER DEFAULT NULL,\n needs_pni_signature INTEGER DEFAULT 0,\n unregistered_timestamp INTEGER DEFAULT 0,\n hidden INTEGER DEFAULT 0,\n reporting_token BLOB DEFAULT NULL,\n system_nickname TEXT DEFAULT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_receipts",
|
||||
sql = "CREATE TABLE group_receipts (\n _id INTEGER PRIMARY KEY, \n mms_id INTEGER, \n address INTEGER, \n status INTEGER, \n timestamp INTEGER, \n unidentified INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "one_time_prekeys",
|
||||
sql = "CREATE TABLE one_time_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL, \n private_key TEXT NOT NULL,\n UNIQUE(account_id, key_id)\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "signed_prekeys",
|
||||
sql = "CREATE TABLE signed_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL,\n private_key TEXT NOT NULL,\n signature TEXT NOT NULL, \n timestamp INTEGER DEFAULT 0,\n UNIQUE(account_id, key_id)\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "sessions",
|
||||
sql = "CREATE TABLE sessions (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n account_id TEXT NOT NULL,\n address TEXT NOT NULL,\n device INTEGER NOT NULL,\n record BLOB NOT NULL,\n UNIQUE(account_id, address, device)\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "sender_keys",
|
||||
sql = "CREATE TABLE sender_keys (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n distribution_id TEXT NOT NULL,\n record BLOB NOT NULL, \n created_at INTEGER NOT NULL, \n UNIQUE(address,device, distribution_id) ON CONFLICT REPLACE\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "sender_key_shared",
|
||||
sql = "CREATE TABLE sender_key_shared (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n distribution_id TEXT NOT NULL, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n timestamp INTEGER DEFAULT 0, \n UNIQUE(distribution_id,address, device) ON CONFLICT REPLACE\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "pending_retry_receipts",
|
||||
sql = "CREATE TABLE pending_retry_receipts(_id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, device INTEGER NOT NULL, sent_timestamp INTEGER NOT NULL, received_timestamp TEXT NOT NULL, thread_id INTEGER NOT NULL, UNIQUE(author,sent_timestamp) ON CONFLICT REPLACE)"
|
||||
),
|
||||
Statement(
|
||||
name = "sticker",
|
||||
sql = "CREATE TABLE sticker (_id INTEGER PRIMARY KEY AUTOINCREMENT, pack_id TEXT NOT NULL, pack_key TEXT NOT NULL, pack_title TEXT NOT NULL, pack_author TEXT NOT NULL, sticker_id INTEGER, cover INTEGER, pack_order INTEGER, emoji TEXT NOT NULL, content_type TEXT DEFAULT NULL, last_used INTEGER, installed INTEGER,file_path TEXT NOT NULL, file_length INTEGER, file_random BLOB, UNIQUE(pack_id, sticker_id, cover) ON CONFLICT IGNORE)"
|
||||
),
|
||||
Statement(
|
||||
name = "storage_key",
|
||||
sql = "CREATE TABLE storage_key (_id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER, key TEXT UNIQUE)"
|
||||
),
|
||||
Statement(
|
||||
name = "mention",
|
||||
sql = "CREATE TABLE mention(_id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, message_id INTEGER, recipient_id INTEGER, range_start INTEGER, range_length INTEGER)"
|
||||
),
|
||||
Statement(
|
||||
name = "payments",
|
||||
sql = "CREATE TABLE payments(_id INTEGER PRIMARY KEY, uuid TEXT DEFAULT NULL, recipient INTEGER DEFAULT 0, recipient_address TEXT DEFAULT NULL, timestamp INTEGER, note TEXT DEFAULT NULL, direction INTEGER, state INTEGER, failure_reason INTEGER, amount BLOB NOT NULL, fee BLOB NOT NULL, transaction_record BLOB DEFAULT NULL, receipt BLOB DEFAULT NULL, payment_metadata BLOB DEFAULT NULL, receipt_public_key TEXT DEFAULT NULL, block_index INTEGER DEFAULT 0, block_timestamp INTEGER DEFAULT 0, seen INTEGER, UNIQUE(uuid) ON CONFLICT ABORT)"
|
||||
),
|
||||
Statement(
|
||||
name = "chat_colors",
|
||||
sql = "CREATE TABLE chat_colors (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n chat_colors BLOB\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "emoji_search",
|
||||
sql = "CREATE TABLE emoji_search (\n _id INTEGER PRIMARY KEY,\n label TEXT NOT NULL,\n emoji TEXT NOT NULL,\n rank INTEGER DEFAULT 2147483647 \n )"
|
||||
),
|
||||
Statement(
|
||||
name = "avatar_picker",
|
||||
sql = "CREATE TABLE avatar_picker (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n last_used INTEGER DEFAULT 0,\n group_id TEXT DEFAULT NULL,\n avatar BLOB NOT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_call_ring",
|
||||
sql = "CREATE TABLE group_call_ring (\n _id INTEGER PRIMARY KEY,\n ring_id INTEGER UNIQUE,\n date_received INTEGER,\n ring_state INTEGER\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "reaction",
|
||||
sql = "CREATE TABLE reaction (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n emoji TEXT NOT NULL,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n UNIQUE(message_id, author_id) ON CONFLICT REPLACE\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "donation_receipt",
|
||||
sql = "CREATE TABLE donation_receipt (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n receipt_type TEXT NOT NULL,\n receipt_date INTEGER NOT NULL,\n amount TEXT NOT NULL,\n currency TEXT NOT NULL,\n subscription_level INTEGER NOT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "story_sends",
|
||||
sql = "CREATE TABLE story_sends (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n allows_replies INTEGER NOT NULL,\n distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "cds",
|
||||
sql = "CREATE TABLE cds (\n _id INTEGER PRIMARY KEY,\n e164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE,\n last_seen_at INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "remote_megaphone",
|
||||
sql = "CREATE TABLE remote_megaphone (\n _id INTEGER PRIMARY KEY,\n uuid TEXT UNIQUE NOT NULL,\n priority INTEGER NOT NULL,\n countries TEXT,\n minimum_version INTEGER NOT NULL,\n dont_show_before INTEGER NOT NULL,\n dont_show_after INTEGER NOT NULL,\n show_for_days INTEGER NOT NULL,\n conditional_id TEXT,\n primary_action_id TEXT,\n secondary_action_id TEXT,\n image_url TEXT,\n image_uri TEXT DEFAULT NULL,\n title TEXT NOT NULL,\n body TEXT NOT NULL,\n primary_action_text TEXT,\n secondary_action_text TEXT,\n shown_at INTEGER DEFAULT 0,\n finished_at INTEGER DEFAULT 0,\n primary_action_data TEXT DEFAULT NULL,\n secondary_action_data TEXT DEFAULT NULL,\n snoozed_at INTEGER DEFAULT 0,\n seen_count INTEGER DEFAULT 0\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "pending_pni_signature_message",
|
||||
sql = "CREATE TABLE pending_pni_signature_message (\n _id INTEGER PRIMARY KEY,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n device_id INTEGER NOT NULL\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "call",
|
||||
sql = "CREATE TABLE call (\n _id INTEGER PRIMARY KEY,\n call_id INTEGER NOT NULL UNIQUE,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n peer INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n type INTEGER NOT NULL,\n direction INTEGER NOT NULL,\n event INTEGER NOT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "message_fts",
|
||||
sql = "CREATE VIRTUAL TABLE message_fts USING fts5(body, thread_id UNINDEXED, content=message, content_rowid=_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "remapped_recipients",
|
||||
sql = "CREATE TABLE remapped_recipients (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "remapped_threads",
|
||||
sql = "CREATE TABLE remapped_threads (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_payload",
|
||||
sql = "CREATE TABLE msl_payload (\n _id INTEGER PRIMARY KEY,\n date_sent INTEGER NOT NULL,\n content BLOB NOT NULL,\n content_hint INTEGER NOT NULL,\n urgent INTEGER NOT NULL DEFAULT 1\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_recipient",
|
||||
sql = "CREATE TABLE msl_recipient (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL, \n device INTEGER NOT NULL\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_message",
|
||||
sql = "CREATE TABLE msl_message (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n message_id INTEGER NOT NULL\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile",
|
||||
sql = "CREATE TABLE notification_profile (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n emoji TEXT NOT NULL,\n color TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n allow_all_calls INTEGER NOT NULL DEFAULT 0,\n allow_all_mentions INTEGER NOT NULL DEFAULT 0\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile_schedule",
|
||||
sql = "CREATE TABLE notification_profile_schedule (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n enabled INTEGER NOT NULL DEFAULT 0,\n start INTEGER NOT NULL,\n end INTEGER NOT NULL,\n days_enabled TEXT NOT NULL\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile_allowed_members",
|
||||
sql = "CREATE TABLE notification_profile_allowed_members (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL,\n UNIQUE(notification_profile_id, recipient_id) ON CONFLICT REPLACE\n)"
|
||||
),
|
||||
Statement(
|
||||
name = "distribution_list",
|
||||
sql = "CREATE TABLE distribution_list (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL,\n distribution_id TEXT UNIQUE NOT NULL,\n recipient_id INTEGER UNIQUE REFERENCES recipient (_id),\n allows_replies INTEGER DEFAULT 1,\n deletion_timestamp INTEGER DEFAULT 0,\n is_unknown INTEGER DEFAULT 0,\n privacy_mode INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "distribution_list_member",
|
||||
sql = "CREATE TABLE distribution_list_member (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id),\n privacy_mode INTEGER DEFAULT 0\n )"
|
||||
),
|
||||
Statement(
|
||||
name = "recipient_group_type_index",
|
||||
sql = "CREATE INDEX recipient_group_type_index ON recipient (group_type)"
|
||||
),
|
||||
Statement(
|
||||
name = "recipient_pni_index",
|
||||
sql = "CREATE UNIQUE INDEX recipient_pni_index ON recipient (pni)"
|
||||
),
|
||||
Statement(
|
||||
name = "recipient_service_id_profile_key",
|
||||
sql = "CREATE INDEX recipient_service_id_profile_key ON recipient (uuid, profile_key) WHERE uuid NOT NULL AND profile_key NOT NULL"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_read_and_notified_and_thread_id_index",
|
||||
sql = "CREATE INDEX mms_read_and_notified_and_thread_id_index ON message (read, notified, thread_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_type_index",
|
||||
sql = "CREATE INDEX mms_type_index ON message (type)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_date_sent_index",
|
||||
sql = "CREATE INDEX mms_date_sent_index ON message (date_sent, recipient_id, thread_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_date_server_index",
|
||||
sql = "CREATE INDEX mms_date_server_index ON message (date_server)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_thread_date_index",
|
||||
sql = "CREATE INDEX mms_thread_date_index ON message (thread_id, date_received)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_reactions_unread_index",
|
||||
sql = "CREATE INDEX mms_reactions_unread_index ON message (reactions_unread)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_story_type_index",
|
||||
sql = "CREATE INDEX mms_story_type_index ON message (story_type)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_parent_story_id_index",
|
||||
sql = "CREATE INDEX mms_parent_story_id_index ON message (parent_story_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_thread_story_parent_story_scheduled_date_index",
|
||||
sql = "CREATE INDEX mms_thread_story_parent_story_scheduled_date_index ON message (thread_id, date_received, story_type, parent_story_id, scheduled_date)"
|
||||
),
|
||||
Statement(
|
||||
name = "message_quote_id_quote_author_scheduled_date_index",
|
||||
sql = "CREATE INDEX message_quote_id_quote_author_scheduled_date_index ON message (quote_id, quote_author, scheduled_date)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_exported_index",
|
||||
sql = "CREATE INDEX mms_exported_index ON message (exported)"
|
||||
),
|
||||
Statement(
|
||||
name = "mms_id_type_payment_transactions_index",
|
||||
sql = "CREATE INDEX mms_id_type_payment_transactions_index ON message (_id,type) WHERE type & 12884901888 != 0"
|
||||
),
|
||||
Statement(
|
||||
name = "part_mms_id_index",
|
||||
sql = "CREATE INDEX part_mms_id_index ON part (mid)"
|
||||
),
|
||||
Statement(
|
||||
name = "pending_push_index",
|
||||
sql = "CREATE INDEX pending_push_index ON part (pending_push)"
|
||||
),
|
||||
Statement(
|
||||
name = "part_sticker_pack_id_index",
|
||||
sql = "CREATE INDEX part_sticker_pack_id_index ON part (sticker_pack_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "part_data_hash_index",
|
||||
sql = "CREATE INDEX part_data_hash_index ON part (data_hash)"
|
||||
),
|
||||
Statement(
|
||||
name = "part_data_index",
|
||||
sql = "CREATE INDEX part_data_index ON part (_data)"
|
||||
),
|
||||
Statement(
|
||||
name = "thread_recipient_id_index",
|
||||
sql = "CREATE INDEX thread_recipient_id_index ON thread (recipient_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "archived_count_index",
|
||||
sql = "CREATE INDEX archived_count_index ON thread (archived, meaningful_messages)"
|
||||
),
|
||||
Statement(
|
||||
name = "thread_pinned_index",
|
||||
sql = "CREATE INDEX thread_pinned_index ON thread (pinned)"
|
||||
),
|
||||
Statement(
|
||||
name = "thread_read",
|
||||
sql = "CREATE INDEX thread_read ON thread (read)"
|
||||
),
|
||||
Statement(
|
||||
name = "draft_thread_index",
|
||||
sql = "CREATE INDEX draft_thread_index ON drafts (thread_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_id_index",
|
||||
sql = "CREATE UNIQUE INDEX group_id_index ON groups (group_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_recipient_id_index",
|
||||
sql = "CREATE UNIQUE INDEX group_recipient_id_index ON groups (recipient_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "expected_v2_id_index",
|
||||
sql = "CREATE UNIQUE INDEX expected_v2_id_index ON groups (expected_v2_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_distribution_id_index",
|
||||
sql = "CREATE UNIQUE INDEX group_distribution_id_index ON groups(distribution_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "group_receipt_mms_id_index",
|
||||
sql = "CREATE INDEX group_receipt_mms_id_index ON group_receipts (mms_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "sticker_pack_id_index",
|
||||
sql = "CREATE INDEX sticker_pack_id_index ON sticker (pack_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "sticker_sticker_id_index",
|
||||
sql = "CREATE INDEX sticker_sticker_id_index ON sticker (sticker_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "storage_key_type_index",
|
||||
sql = "CREATE INDEX storage_key_type_index ON storage_key (type)"
|
||||
),
|
||||
Statement(
|
||||
name = "mention_message_id_index",
|
||||
sql = "CREATE INDEX mention_message_id_index ON mention (message_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "mention_recipient_id_thread_id_index",
|
||||
sql = "CREATE INDEX mention_recipient_id_thread_id_index ON mention (recipient_id, thread_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "timestamp_direction_index",
|
||||
sql = "CREATE INDEX timestamp_direction_index ON payments (timestamp, direction)"
|
||||
),
|
||||
Statement(
|
||||
name = "timestamp_index",
|
||||
sql = "CREATE INDEX timestamp_index ON payments (timestamp)"
|
||||
),
|
||||
Statement(
|
||||
name = "receipt_public_key_index",
|
||||
sql = "CREATE UNIQUE INDEX receipt_public_key_index ON payments (receipt_public_key)"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_payload_date_sent_index",
|
||||
sql = "CREATE INDEX msl_payload_date_sent_index ON msl_payload (date_sent)"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_recipient_recipient_index",
|
||||
sql = "CREATE INDEX msl_recipient_recipient_index ON msl_recipient (recipient_id, device, payload_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_recipient_payload_index",
|
||||
sql = "CREATE INDEX msl_recipient_payload_index ON msl_recipient (payload_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_message_message_index",
|
||||
sql = "CREATE INDEX msl_message_message_index ON msl_message (message_id, payload_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "date_received_index",
|
||||
sql = "CREATE INDEX date_received_index on group_call_ring (date_received)"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile_schedule_profile_index",
|
||||
sql = "CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "notification_profile_allowed_members_profile_index",
|
||||
sql = "CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "donation_receipt_type_index",
|
||||
sql = "CREATE INDEX donation_receipt_type_index ON donation_receipt (receipt_type)"
|
||||
),
|
||||
Statement(
|
||||
name = "donation_receipt_date_index",
|
||||
sql = "CREATE INDEX donation_receipt_date_index ON donation_receipt (receipt_date)"
|
||||
),
|
||||
Statement(
|
||||
name = "story_sends_recipient_id_sent_timestamp_allows_replies_index",
|
||||
sql = "CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)"
|
||||
),
|
||||
Statement(
|
||||
name = "story_sends_message_id_distribution_id_index",
|
||||
sql = "CREATE INDEX story_sends_message_id_distribution_id_index ON story_sends (message_id, distribution_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "distribution_list_member_list_id_recipient_id_privacy_mode_index",
|
||||
sql = "CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)"
|
||||
),
|
||||
Statement(
|
||||
name = "pending_pni_recipient_sent_device_index",
|
||||
sql = "CREATE UNIQUE INDEX pending_pni_recipient_sent_device_index ON pending_pni_signature_message (recipient_id, sent_timestamp, device_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "call_call_id_index",
|
||||
sql = "CREATE INDEX call_call_id_index ON call (call_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "call_message_id_index",
|
||||
sql = "CREATE INDEX call_message_id_index ON call (message_id)"
|
||||
),
|
||||
Statement(
|
||||
name = "message_ai",
|
||||
sql = "CREATE TRIGGER message_ai AFTER INSERT ON message BEGIN\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END"
|
||||
),
|
||||
Statement(
|
||||
name = "message_ad",
|
||||
sql = "CREATE TRIGGER message_ad AFTER DELETE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n END"
|
||||
),
|
||||
Statement(
|
||||
name = "message_au",
|
||||
sql = "CREATE TRIGGER message_au AFTER UPDATE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_message_delete",
|
||||
sql = "CREATE TRIGGER msl_message_delete AFTER DELETE ON message \n BEGIN \n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id);\n END"
|
||||
),
|
||||
Statement(
|
||||
name = "msl_attachment_delete",
|
||||
sql = "CREATE TRIGGER msl_attachment_delete AFTER DELETE ON part\n BEGIN\n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old.mid);\n END"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,37 @@ class GroupTableTest {
|
||||
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroup_whenIRemapRecipientsThatHaveAConflict_thenIExpectDeletion() {
|
||||
val v2Group = insertPushGroupWithSelfAndOthers(
|
||||
listOf(
|
||||
harness.others[0],
|
||||
harness.others[1]
|
||||
)
|
||||
)
|
||||
|
||||
insertThread(v2Group)
|
||||
|
||||
groupTable.remapRecipient(harness.others[0], harness.others[1])
|
||||
|
||||
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||
|
||||
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroup_whenIRemapRecipients_thenIExpectRemap() {
|
||||
val v2Group = insertPushGroup()
|
||||
insertThread(v2Group)
|
||||
|
||||
val newId = harness.others[1]
|
||||
groupTable.remapRecipient(harness.others[0], newId)
|
||||
|
||||
val groupRecord = groupTable.getGroup(v2Group).get()
|
||||
|
||||
assertEquals(setOf(harness.self.id, newId), groupRecord.members.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenAGroupAndMember_whenIIsCurrentMember_thenIExpectTrue() {
|
||||
val v2Group = insertPushGroup()
|
||||
@@ -280,6 +311,31 @@ class GroupTableTest {
|
||||
.setRevision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)!!
|
||||
}
|
||||
|
||||
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
|
||||
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
|
||||
|
||||
val selfMember: DecryptedMember = DecryptedMember.newBuilder()
|
||||
.setUuid(harness.self.requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
|
||||
val otherMembers: List<DecryptedMember> = others.map { id ->
|
||||
DecryptedMember.newBuilder()
|
||||
.setUuid(Recipient.resolved(id).requireServiceId().toByteString())
|
||||
.setJoinedAtRevision(0)
|
||||
.setRole(Member.Role.DEFAULT)
|
||||
.build()
|
||||
}
|
||||
|
||||
val decryptedGroupState = DecryptedGroup.newBuilder()
|
||||
.addAllMembers(listOf(selfMember) + otherMembers)
|
||||
.setRevision(0)
|
||||
.build()
|
||||
|
||||
return groupTable.create(groupMasterKey, decryptedGroupState)!!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class MessageTableTest_gifts {
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
@@ -102,7 +102,7 @@ class MessageTableTest_gifts {
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
@@ -121,13 +121,13 @@ class MessageTableTest_gifts {
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 3,
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
@@ -146,13 +146,13 @@ class MessageTableTest_gifts {
|
||||
|
||||
val messageId2 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 3,
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
@@ -171,13 +171,13 @@ class MessageTableTest_gifts {
|
||||
|
||||
MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 2,
|
||||
giftBadge = GiftBadge.getDefaultInstance()
|
||||
)
|
||||
|
||||
val messageId3 = MmsHelper.insert(
|
||||
recipient = Recipient.resolved(recipients[0]),
|
||||
sentTimeMillis = 1,
|
||||
sentTimeMillis = 3,
|
||||
giftBadge = null
|
||||
)
|
||||
|
||||
|
||||
@@ -253,8 +253,7 @@ class MmsTableTest_stories {
|
||||
val groupStoryId = MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = -1L
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
)
|
||||
|
||||
// WHEN
|
||||
@@ -319,8 +318,7 @@ class MmsTableTest_stories {
|
||||
val groupStoryId = MmsHelper.insert(
|
||||
recipient = myStory,
|
||||
sentTimeMillis = 200,
|
||||
storyType = StoryType.STORY_WITH_REPLIES,
|
||||
threadId = -1L
|
||||
storyType = StoryType.STORY_WITH_REPLIES
|
||||
)
|
||||
|
||||
MmsHelper.insert(
|
||||
@@ -331,7 +329,7 @@ class MmsTableTest_stories {
|
||||
receivedTimeMillis = 202,
|
||||
parentStoryId = ParentStoryId.GroupReply(groupStoryId)
|
||||
),
|
||||
-1
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(myStory, ThreadTable.DistributionTypes.DEFAULT)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientTableTest_applyStorageSyncContactUpdate {
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Test
|
||||
fun insertMessageOnVerifiedToDefault() {
|
||||
// GIVEN
|
||||
val identities = ApplicationDependencies.getProtocolStore().aci().identities()
|
||||
val other = Recipient.resolved(harness.others[0])
|
||||
|
||||
MmsHelper.insert(recipient = other)
|
||||
identities.setVerified(other.id, harness.othersKeys[0].publicKey, IdentityTable.VerifiedStatus.VERIFIED)
|
||||
|
||||
val oldRecord: SignalContactRecord = StorageSyncModels.localToRemoteRecord(SignalDatabase.recipients.getRecordForSync(harness.others[0])!!).contact.get()
|
||||
|
||||
val newProto = oldRecord
|
||||
.toProto()
|
||||
.toBuilder()
|
||||
.setIdentityState(ContactRecord.IdentityState.DEFAULT)
|
||||
.build()
|
||||
val newRecord = SignalContactRecord(oldRecord.id, newProto)
|
||||
|
||||
val update = StorageRecordUpdate<SignalContactRecord>(oldRecord, newRecord)
|
||||
|
||||
// WHEN
|
||||
val oldVerifiedStatus: IdentityTable.VerifiedStatus = identities.getIdentityRecord(other.id).get().verifiedStatus
|
||||
SignalDatabase.recipients.applyStorageSyncContactUpdate(update)
|
||||
val newVerifiedStatus: IdentityTable.VerifiedStatus = identities.getIdentityRecord(other.id).get().verifiedStatus
|
||||
|
||||
// THEN
|
||||
oldVerifiedStatus assertIs IdentityTable.VerifiedStatus.VERIFIED
|
||||
newVerifiedStatus assertIs IdentityTable.VerifiedStatus.DEFAULT
|
||||
|
||||
val messages = MessageTableTestUtils.getMessages(SignalDatabase.threads.getThreadIdFor(other.id)!!)
|
||||
messages.first().isIdentityDefault assertIs true
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class RecipientTableTest_getAndPossiblyMerge {
|
||||
|
||||
@@ -693,9 +694,9 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
val sms2: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId2)!!
|
||||
val sms3: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId3)!!
|
||||
|
||||
assertEquals(retrievedId, sms1.recipient.id)
|
||||
assertEquals(retrievedId, sms2.recipient.id)
|
||||
assertEquals(retrievedId, sms3.recipient.id)
|
||||
assertEquals(retrievedId, sms1.fromRecipient.id)
|
||||
assertEquals(retrievedId, sms2.fromRecipient.id)
|
||||
assertEquals(retrievedId, sms3.fromRecipient.id)
|
||||
|
||||
assertEquals(retrievedThreadId, sms1.threadId)
|
||||
assertEquals(retrievedThreadId, sms2.threadId)
|
||||
@@ -706,9 +707,9 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
val mms2: MessageRecord = SignalDatabase.messages.getMessageRecord(mmsId2)!!
|
||||
val mms3: MessageRecord = SignalDatabase.messages.getMessageRecord(mmsId3)!!
|
||||
|
||||
assertEquals(retrievedId, mms1.recipient.id)
|
||||
assertEquals(retrievedId, mms2.recipient.id)
|
||||
assertEquals(retrievedId, mms3.recipient.id)
|
||||
assertEquals(retrievedId, mms1.fromRecipient.id)
|
||||
assertEquals(retrievedId, mms2.fromRecipient.id)
|
||||
assertEquals(retrievedId, mms3.fromRecipient.id)
|
||||
|
||||
assertEquals(retrievedThreadId, mms1.threadId)
|
||||
assertEquals(retrievedThreadId, mms2.threadId)
|
||||
@@ -857,6 +858,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
}
|
||||
|
||||
ApplicationDependencies.getRecipientCache().clear()
|
||||
ApplicationDependencies.getRecipientCache().clearSelf()
|
||||
RecipientId.clearCache()
|
||||
}
|
||||
|
||||
@@ -872,7 +874,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
if (createThread) {
|
||||
// Create a thread and throw a dummy message in it so it doesn't get automatically deleted
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(id))
|
||||
SignalDatabase.messages.insertMessageInbox(IncomingEncryptedMessage(IncomingTextMessage(id, 1, 0, 0, 0, "", Optional.empty(), 0, false, ""), ""))
|
||||
SignalDatabase.messages.insertMessageInbox(IncomingEncryptedMessage(IncomingTextMessage(id, 1, (Math.random() * Long.MAX_VALUE).toLong(), 0, 0, "", Optional.empty(), 0, false, ""), ""))
|
||||
}
|
||||
|
||||
if (pniSession) {
|
||||
@@ -1035,7 +1037,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select(MessageTable.BODY)
|
||||
.from(MessageTable.TABLE_NAME)
|
||||
.where("${MessageTable.RECIPIENT_ID} = ? AND ${MessageTable.TYPE} = ?", recipientId, MessageTypes.THREAD_MERGE_TYPE)
|
||||
.where("${MessageTable.FROM_RECIPIENT_ID} = ? AND ${MessageTable.TYPE} = ?", recipientId, MessageTypes.THREAD_MERGE_TYPE)
|
||||
.orderBy("${MessageTable.DATE_RECEIVED} DESC")
|
||||
.limit(1)
|
||||
.run()
|
||||
@@ -1053,7 +1055,7 @@ class RecipientTableTest_getAndPossiblyMerge {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select(MessageTable.BODY)
|
||||
.from(MessageTable.TABLE_NAME)
|
||||
.where("${MessageTable.RECIPIENT_ID} = ? AND ${MessageTable.TYPE} = ?", recipientId, MessageTypes.SESSION_SWITCHOVER_TYPE)
|
||||
.where("${MessageTable.FROM_RECIPIENT_ID} = ? AND ${MessageTable.TYPE} = ?", recipientId, MessageTypes.SESSION_SWITCHOVER_TYPE)
|
||||
.orderBy("${MessageTable.DATE_RECEIVED} DESC")
|
||||
.limit(1)
|
||||
.run()
|
||||
|
||||
@@ -180,4 +180,45 @@ class SQLiteDatabaseTest {
|
||||
assertTrue(hasRun1.get())
|
||||
assertTrue(hasRun2.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun runPostSuccessfulTransaction_runsAfterMainTransactionInNestedTransaction() {
|
||||
val hasRun1 = AtomicBoolean(false)
|
||||
val hasRun2 = AtomicBoolean(false)
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
hasRun1.set(true)
|
||||
}
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.beginTransaction()
|
||||
|
||||
db.runPostSuccessfulTransaction {
|
||||
assertTrue(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
hasRun2.set(true)
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful()
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.endTransaction()
|
||||
db.setTransactionSuccessful()
|
||||
|
||||
assertFalse(hasRun1.get())
|
||||
assertFalse(hasRun2.get())
|
||||
|
||||
db.endTransaction()
|
||||
|
||||
assertTrue(hasRun1.get())
|
||||
assertTrue(hasRun2.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
@@ -25,6 +27,9 @@ import java.util.UUID
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class StorySendTableTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule(othersCount = 0, createGroup = false)
|
||||
|
||||
private val distributionId1 = DistributionId.from(UUID.randomUUID())
|
||||
private val distributionId2 = DistributionId.from(UUID.randomUUID())
|
||||
private val distributionId3 = DistributionId.from(UUID.randomUUID())
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("ClassName")
|
||||
class ThreadTableTest_active {
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val databaseRule = SignalDatabaseRule()
|
||||
|
||||
private lateinit var recipient: Recipient
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
|
||||
SignalDatabase.threads.getUnarchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
false,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(1, threads.count)
|
||||
|
||||
val record = ThreadTable.StaticReader(threads, InstrumentationRegistry.getInstrumentation().context).getNext()
|
||||
|
||||
assertNotNull(record)
|
||||
assertEquals(record!!.recipient.id, recipient.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenInactiveUnarchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
|
||||
SignalDatabase.threads.getUnarchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
false,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(0, threads.count)
|
||||
}
|
||||
|
||||
val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
assertEquals(threadId2, threadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveArchivedThread_whenIGetUnarchivedConversationList_thenIExpectNoThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), true)
|
||||
|
||||
SignalDatabase.threads.getUnarchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
false,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(0, threads.count)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveArchivedThread_whenIGetArchivedConversationList_thenIExpectThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), true)
|
||||
|
||||
SignalDatabase.threads.getArchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(1, threads.count)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenInactiveArchivedThread_whenIGetArchivedConversationList_thenIExpectNoThread() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), true)
|
||||
|
||||
SignalDatabase.threads.getArchivedConversationList(
|
||||
ConversationFilter.OFF,
|
||||
0,
|
||||
10
|
||||
).use { threads ->
|
||||
assertEquals(0, threads.count)
|
||||
}
|
||||
|
||||
val threadId2 = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
assertEquals(threadId2, threadId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenActiveArchivedThread_whenIDeactivateThread_thenIExpectNoMessages() {
|
||||
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
|
||||
MmsHelper.insert(recipient = recipient, threadId = threadId)
|
||||
SignalDatabase.threads.update(threadId, false)
|
||||
|
||||
SignalDatabase.messages.getConversation(threadId).use {
|
||||
assertEquals(1, it.count)
|
||||
}
|
||||
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
|
||||
SignalDatabase.messages.getConversation(threadId).use {
|
||||
assertEquals(0, it.count)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupSe
|
||||
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
|
||||
|
||||
@@ -74,18 +75,20 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
|
||||
|
||||
serviceTrustStore = SignalServiceTrustStore(application)
|
||||
uncensoredConfiguration = SignalServiceConfiguration(
|
||||
arrayOf(SignalServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
mapOf(
|
||||
signalServiceUrls = arrayOf(SignalServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
signalCdnUrlMap = mapOf(
|
||||
0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT))
|
||||
),
|
||||
arrayOf(SignalKeyBackupServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
|
||||
emptyList(),
|
||||
Optional.of(SignalServiceNetworkAccess.DNS),
|
||||
Optional.empty(),
|
||||
Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS)
|
||||
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)),
|
||||
networkInterceptors = emptyList(),
|
||||
dns = Optional.of(SignalServiceNetworkAccess.DNS),
|
||||
signalProxy = Optional.empty(),
|
||||
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
|
||||
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS)
|
||||
)
|
||||
|
||||
serviceNetworkAccessMock = mock {
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.libsignal.protocol.ecc.Curve
|
||||
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.testing.Get
|
||||
import org.thoughtcrime.securesms.testing.Put
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.testing.assertIsNot
|
||||
import org.thoughtcrime.securesms.testing.parsedRequestBody
|
||||
import org.thoughtcrime.securesms.testing.success
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyState
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyStatus
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PreKeysSyncJobTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private val aciPreKeyMeta: PreKeyMetadataStore
|
||||
get() = SignalStore.account().aciPreKeys
|
||||
|
||||
private val pniPreKeyMeta: PreKeyMetadataStore
|
||||
get() = SignalStore.account().pniPreKeys
|
||||
|
||||
private lateinit var job: PreKeysSyncJob
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
job = PreKeysSyncJob()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create signed prekeys for both identities when both do not have registered prekeys according
|
||||
* to our local state.
|
||||
*/
|
||||
@Test
|
||||
fun runWithoutRegisteredKeysForBothIdentities() {
|
||||
// GIVEN
|
||||
aciPreKeyMeta.isSignedPreKeyRegistered = false
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered = false
|
||||
|
||||
lateinit var aciSignedPreKey: SignedPreKeyEntity
|
||||
lateinit var pniSignedPreKey: SignedPreKeyEntity
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Put("/v2/keys/signed?identity=aci") { r ->
|
||||
aciSignedPreKey = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Put("/v2/keys/signed?identity=pni") { r ->
|
||||
pniSignedPreKey = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
|
||||
aciPreKeyMeta.isSignedPreKeyRegistered assertIs true
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered assertIs true
|
||||
|
||||
val aciVerifySignatureResult = Curve.verifySignature(
|
||||
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.publicKey,
|
||||
aciSignedPreKey.publicKey.serialize(),
|
||||
aciSignedPreKey.signature
|
||||
)
|
||||
aciVerifySignatureResult assertIs true
|
||||
|
||||
val pniVerifySignatureResult = Curve.verifySignature(
|
||||
ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.publicKey,
|
||||
pniSignedPreKey.publicKey.serialize(),
|
||||
pniSignedPreKey.signature
|
||||
)
|
||||
pniVerifySignatureResult assertIs true
|
||||
}
|
||||
|
||||
/**
|
||||
* With 100 prekeys registered for each identity, do nothing.
|
||||
*/
|
||||
@Test
|
||||
fun runWithRegisteredKeysForBothIdentities() {
|
||||
// GIVEN
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(100)) }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIs currentPniKeyId
|
||||
}
|
||||
|
||||
/**
|
||||
* With 100 prekeys registered for ACI, but no PNI prekeys registered according to local state,
|
||||
* do nothing for ACI but create PNI prekeys and update local state.
|
||||
*/
|
||||
@Test
|
||||
fun runWithRegisteredKeysForAciIdentityOnly() {
|
||||
// GIVEN
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered = false
|
||||
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
|
||||
Put("/v2/keys/signed?identity=pni") { MockResponse().success() }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
|
||||
pniPreKeyMeta.isSignedPreKeyRegistered assertIs true
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
|
||||
}
|
||||
|
||||
/**
|
||||
* With <10 prekeys registered for each identity, upload new.
|
||||
*/
|
||||
@Test
|
||||
fun runWithLowNumberOfRegisteredKeysForBothIdentities() {
|
||||
// GIVEN
|
||||
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
|
||||
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
|
||||
|
||||
val currentNextAciPreKeyId = aciPreKeyMeta.nextOneTimePreKeyId
|
||||
val currentNextPniPreKeyId = pniPreKeyMeta.nextOneTimePreKeyId
|
||||
|
||||
lateinit var aciPreKeyStateRequest: PreKeyState
|
||||
lateinit var pniPreKeyStateRequest: PreKeyState
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(5)) },
|
||||
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(5)) },
|
||||
Put("/v2/keys/?identity=aci") { r ->
|
||||
aciPreKeyStateRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
},
|
||||
Put("/v2/keys/?identity=pni") { r ->
|
||||
pniPreKeyStateRequest = r.parsedRequestBody()
|
||||
MockResponse().success()
|
||||
}
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result: Job.Result = job.run()
|
||||
|
||||
// THEN
|
||||
result.isSuccess assertIs true
|
||||
aciPreKeyMeta.activeSignedPreKeyId assertIsNot currentAciKeyId
|
||||
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
|
||||
|
||||
aciPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextAciPreKeyId
|
||||
pniPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextPniPreKeyId
|
||||
|
||||
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.let { aciIdentityKey ->
|
||||
aciPreKeyStateRequest.identityKey assertIs aciIdentityKey
|
||||
|
||||
val verifySignatureResult = Curve.verifySignature(
|
||||
aciIdentityKey.publicKey,
|
||||
aciPreKeyStateRequest.signedPreKey.publicKey.serialize(),
|
||||
aciPreKeyStateRequest.signedPreKey.signature
|
||||
)
|
||||
verifySignatureResult assertIs true
|
||||
}
|
||||
|
||||
ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.let { pniIdentityKey ->
|
||||
pniPreKeyStateRequest.identityKey assertIs pniIdentityKey
|
||||
|
||||
val verifySignatureResult = Curve.verifySignature(
|
||||
pniIdentityKey.publicKey,
|
||||
pniPreKeyStateRequest.signedPreKey.publicKey.serialize(),
|
||||
pniPreKeyStateRequest.signedPreKey.signature
|
||||
)
|
||||
verifySignatureResult assertIs true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -36,6 +37,11 @@ class RefreshOwnProfileJob__checkUsernameIsInSyncTest {
|
||||
|
||||
@Test
|
||||
fun givenNoLocalUsername_whenICheckUsernameIsInSync_thenIExpectNoFailures() {
|
||||
// GIVEN
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Delete("/v1/accounts/username_hash") { MockResponse().success() }
|
||||
)
|
||||
|
||||
// WHEN
|
||||
RefreshOwnProfileJob.checkUsernameIsInSync()
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.signal.core.util.Hex;
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin;
|
||||
import org.signal.libsignal.svr2.PinHash;
|
||||
import org.whispersystems.signalservice.api.kbs.KbsData;
|
||||
import org.whispersystems.signalservice.api.kbs.MasterKey;
|
||||
import org.whispersystems.signalservice.api.kbs.PinHashUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -21,77 +22,76 @@ public final class PinHashing_hashPin_Test {
|
||||
@Test
|
||||
public void argon2_hashed_pin_password() throws IOException {
|
||||
String pin = "password";
|
||||
byte[] backupId = Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
|
||||
byte[] salt = Hex.fromStringCondensed("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
PinHash hashedPin = PinHashUtil.hashPin(pin, salt);
|
||||
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
|
||||
|
||||
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("ab7e8499d21f80a6600b3b9ee349ac6d72c07e3359fe885a934ba7aa844429f8"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("3f33ce58eb25b40436592a30eae2a8fabab1899095f4e2fba6e2d0dc43b4a2d9cac5a3931748522393951e0e54dec769"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
|
||||
String localPinHash = PinHashing.localPinHash(pin);
|
||||
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
|
||||
String localPinHash = PinHashUtil.localPinHash(pin);
|
||||
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argon2_hashed_pin_another_password() throws IOException {
|
||||
String pin = "anotherpassword";
|
||||
byte[] backupId = Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f");
|
||||
byte[] salt = Hex.fromStringCondensed("202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("88a787415a2ecd79da0d1016a82a27c5c695c9a19b88b0aa1d35683280aa9a67"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
PinHash hashedPin = PinHashUtil.hashPin(pin, salt);
|
||||
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
|
||||
|
||||
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("301d9dd1e96f20ce51083f67d3298fd37b97525de8324d5e12ed2d407d3d927b"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("9d9b05402ea39c17ff1c9298c8a0e86784a352aa02a74943bf8bcf07ec0f4b574a5b786ad0182c8d308d9eb06538b8c9"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
|
||||
String localPinHash = PinHashing.localPinHash(pin);
|
||||
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
|
||||
String localPinHash = PinHashUtil.localPinHash(pin);
|
||||
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argon2_hashed_pin_password_with_spaces_diacritics_and_non_arabic_numerals() throws IOException {
|
||||
String pin = " Pass६örd ";
|
||||
byte[] backupId = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8");
|
||||
byte[] salt = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("9571f3fde1e58588ba49bcf82be1b301ca3859a6f59076f79a8f47181ef952bf"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
PinHash hashedPin = PinHashUtil.hashPin(pin, salt);
|
||||
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
|
||||
|
||||
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("ab645acdccc1652a48a34b2ac6926340ff35c03034013f68760f20013f028dd8"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("11c0ba1834db15e47c172f6c987c64bd4cfc69c6047dd67a022afeec0165a10943f204d5b8f37b3cb7bab21c6dfc39c8"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
|
||||
assertEquals("577939bccb2b6638c39222d5a97998a867c5e154e30b82cc120f2dd07a3de987", kbsData.getMasterKey().deriveRegistrationLock());
|
||||
|
||||
String localPinHash = PinHashing.localPinHash(pin);
|
||||
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
|
||||
String localPinHash = PinHashUtil.localPinHash(pin);
|
||||
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argon2_hashed_pin_password_with_just_non_arabic_numerals() throws IOException {
|
||||
String pin = " ६१८ ";
|
||||
byte[] backupId = Hex.fromStringCondensed("717dc111a98423a57196512606822fca646c653facd037c10728f14ba0be2ab3");
|
||||
byte[] salt = Hex.fromStringCondensed("717dc111a98423a57196512606822fca646c653facd037c10728f14ba0be2ab3");
|
||||
MasterKey masterKey = new MasterKey(Hex.fromStringCondensed("0432d735b32f66d0e3a70d4f9cc821a8529521a4937d26b987715d8eff4e4c54"));
|
||||
|
||||
HashedPin hashedPin = PinHashing.hashPin(pin, () -> backupId);
|
||||
KbsData kbsData = hashedPin.createNewKbsData(masterKey);
|
||||
PinHash hashedPin = PinHashUtil.hashPin(pin, salt);
|
||||
KbsData kbsData = PinHashUtil.createNewKbsData(hashedPin, masterKey);
|
||||
|
||||
|
||||
assertArrayEquals(hashedPin.getKbsAccessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(hashedPin.accessKey(), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("d2fedabd0d4c17a371491c9722578843a26be3b4923e28d452ab2fc5491e794b"), kbsData.getKbsAccessKey());
|
||||
assertArrayEquals(Hex.fromStringCondensed("877ef871ef1fc668401c717ef21aa12e8523579fb1ff4474b76f28c2293537c80cc7569996c9e0229bea7f378e3a824e"), kbsData.getCipherText());
|
||||
assertEquals(masterKey, kbsData.getMasterKey());
|
||||
|
||||
assertEquals("23a75cb1df1a87df45cc2ed167c2bdc85ab1220b847c88761b0005cac907fce5", kbsData.getMasterKey().deriveRegistrationLock());
|
||||
|
||||
String localPinHash = PinHashing.localPinHash(pin);
|
||||
assertTrue(PinHashing.verifyLocalPinHash(localPinHash, pin));
|
||||
String localPinHash = PinHashUtil.localPinHash(pin);
|
||||
assertTrue(PinHashUtil.verifyLocalPinHash(localPinHash, pin));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.database.Cursor
|
||||
import android.util.Base64
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.model.toBodyRangeList
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.thoughtcrime.securesms.util.MessageTableTestUtils
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.EditMessage
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class EditMessageSyncProcessorTest {
|
||||
|
||||
companion object {
|
||||
private val IGNORE_MESSAGE_COLUMNS = listOf(
|
||||
MessageTable.DATE_RECEIVED,
|
||||
MessageTable.NOTIFIED_TIMESTAMP,
|
||||
MessageTable.REACTIONS_LAST_SEEN,
|
||||
MessageTable.NOTIFIED
|
||||
)
|
||||
|
||||
private val IGNORE_ATTACHMENT_COLUMNS = listOf(
|
||||
AttachmentTable.UNIQUE_ID,
|
||||
AttachmentTable.TRANSFER_FILE
|
||||
)
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var processorV2: MessageContentProcessorV2
|
||||
private lateinit var testResult: TestResults
|
||||
private var envelopeTimestamp: Long = 0
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
processorV2 = MessageContentProcessorV2(harness.context)
|
||||
envelopeTimestamp = System.currentTimeMillis()
|
||||
testResult = TestResults()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textMessage() {
|
||||
var originalTimestamp = envelopeTimestamp + 200
|
||||
for (i in 1..10) {
|
||||
originalTimestamp += 400
|
||||
|
||||
val toRecipient = Recipient.resolved(harness.others[0])
|
||||
|
||||
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)
|
||||
)
|
||||
).build()
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer)
|
||||
val syncTextMessage = TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(originalTimestamp),
|
||||
content = syncContent,
|
||||
metadata = metadata,
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(originalTimestamp)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
).build()
|
||||
|
||||
val syncEditMessage = TestMessage(
|
||||
envelope = MessageContentFuzzer.envelope(editTimestamp),
|
||||
content = editSyncContent,
|
||||
metadata = metadata,
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(editTimestamp)
|
||||
)
|
||||
|
||||
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
|
||||
|
||||
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage.expireTimer / 1000)
|
||||
val originalTextMessage = OutgoingMessage(
|
||||
threadRecipient = toRecipient,
|
||||
sentTimeMillis = originalTimestamp,
|
||||
body = content.dataMessage.body,
|
||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = content.dataMessage.bodyRangesList.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) {
|
||||
SignalDatabase.messages.markExpireStarted(originalMessageId, originalTimestamp)
|
||||
}
|
||||
|
||||
val editMessage = OutgoingMessage(
|
||||
threadRecipient = toRecipient,
|
||||
sentTimeMillis = editTimestamp,
|
||||
body = editedContent.dataMessage.body,
|
||||
expiresIn = content.dataMessage.expireTimer.seconds.inWholeMilliseconds,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = editedContent.dataMessage.bodyRangesList.toBodyRangeList(),
|
||||
messageToEdit = originalMessageId
|
||||
)
|
||||
|
||||
val editMessageId = SignalDatabase.messages.insertMessageOutbox(editMessage, threadId, false, null)
|
||||
SignalDatabase.messages.markAsSent(editMessageId, true)
|
||||
|
||||
if (content.dataMessage.expireTimer > 0) {
|
||||
SignalDatabase.messages.markExpireStarted(editMessageId, originalTimestamp)
|
||||
}
|
||||
testResult.collectLocal()
|
||||
testResult.assert()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class TestResults {
|
||||
|
||||
private lateinit var localMessages: List<List<Pair<String, String?>>>
|
||||
private lateinit var localAttachments: List<List<Pair<String, String?>>>
|
||||
|
||||
private lateinit var syncMessages: List<List<Pair<String, String?>>>
|
||||
private lateinit var syncAttachments: List<List<Pair<String, String?>>>
|
||||
|
||||
fun collectLocal() {
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
localMessages = dumpMessages()
|
||||
localAttachments = dumpAttachments()
|
||||
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fun runSync(messages: List<TestMessage>) {
|
||||
messages.forEach { (envelope, content, metadata, serverDeliveredTimestamp) ->
|
||||
if (content.hasSyncMessage()) {
|
||||
processorV2.process(
|
||||
envelope,
|
||||
content,
|
||||
metadata,
|
||||
serverDeliveredTimestamp,
|
||||
false
|
||||
)
|
||||
ThreadUtil.sleep(1)
|
||||
}
|
||||
}
|
||||
harness.inMemoryLogger.clear()
|
||||
|
||||
syncMessages = dumpMessages()
|
||||
syncAttachments = dumpAttachments()
|
||||
|
||||
cleanup()
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
SignalDatabase.rawDatabase.withinTransaction { db ->
|
||||
SignalDatabase.threads.deleteAllConversations()
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${MessageTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${ThreadTable.TABLE_NAME}'")
|
||||
db.execSQL("DELETE FROM sqlite_sequence WHERE name = '${AttachmentTable.TABLE_NAME}'")
|
||||
}
|
||||
}
|
||||
|
||||
fun assert() {
|
||||
syncMessages.zip(localMessages)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
|
||||
syncAttachments.zip(localAttachments)
|
||||
.forEach { (v2, v1) ->
|
||||
v2.assertIs(v1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpMessages(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(MessageTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_MESSAGE_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpAttachments(): List<List<Pair<String, String?>>> {
|
||||
return dumpTable(AttachmentTable.TABLE_NAME)
|
||||
.map { row ->
|
||||
val newRow = row.toMutableList()
|
||||
newRow.removeIf { IGNORE_ATTACHMENT_COLUMNS.contains(it.first) }
|
||||
newRow
|
||||
}
|
||||
}
|
||||
|
||||
private fun dumpTable(table: String): List<List<Pair<String, String?>>> {
|
||||
return SignalDatabase.rawDatabase
|
||||
.select()
|
||||
.from(table)
|
||||
.run()
|
||||
.readToList { cursor ->
|
||||
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
|
||||
val index = cursor.getColumnIndex(column)
|
||||
var data: String? = when (cursor.getType(index)) {
|
||||
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
|
||||
else -> cursor.getString(index)
|
||||
}
|
||||
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
|
||||
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
|
||||
}
|
||||
|
||||
column to data
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.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
|
||||
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
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MessageContentProcessorV2__recipientStatusTest {
|
||||
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
private lateinit var processorV2: MessageContentProcessorV2
|
||||
private var envelopeTimestamp: Long = 0
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
processorV2 = MessageContentProcessorV2(harness.context)
|
||||
envelopeTimestamp = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
/**
|
||||
* Process sync group sent text transcript with partial send and then process second sync with recipient update
|
||||
* flag set to true with the rest of the send completed.
|
||||
*/
|
||||
@Test
|
||||
fun syncGroupSentTextMessageWithRecipientUpdateFollowup() {
|
||||
val (groupId, masterKey, groupRecipientId) = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), harness.others[0].asMember(), harness.others[1].asMember())
|
||||
val groupContextV2 = GroupContextV2.newBuilder().setRevision(0).setMasterKey(masterKey.serialize().toProtoByteString()).build()
|
||||
|
||||
val initialTextMessage = DataMessage.newBuilder().buildWith {
|
||||
body = MessageContentFuzzer.string()
|
||||
groupV2 = groupContextV2
|
||||
timestamp = envelopeTimestamp
|
||||
}
|
||||
|
||||
processorV2.process(
|
||||
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
|
||||
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])),
|
||||
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
val threadId = SignalDatabase.threads.getThreadIdFor(groupRecipientId)!!
|
||||
val firstSyncMessages = MessageTableTestUtils.getMessages(threadId)
|
||||
val firstMessageId = firstSyncMessages[0].id
|
||||
val firstReceiptInfo = SignalDatabase.groupReceipts.getGroupReceiptInfo(firstMessageId)
|
||||
|
||||
processorV2.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),
|
||||
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
|
||||
)
|
||||
|
||||
val secondSyncMessages = MessageTableTestUtils.getMessages(threadId)
|
||||
val secondReceiptInfo = SignalDatabase.groupReceipts.getGroupReceiptInfo(firstMessageId)
|
||||
|
||||
firstSyncMessages.size assertIs 1
|
||||
firstSyncMessages[0].body assertIs initialTextMessage.body
|
||||
firstReceiptInfo.first { it.recipientId == harness.others[0] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED
|
||||
firstReceiptInfo.first { it.recipientId == harness.others[1] }.status assertIs GroupReceiptTable.STATUS_UNKNOWN
|
||||
|
||||
secondSyncMessages.size assertIs 1
|
||||
secondSyncMessages[0].body assertIs initialTextMessage.body
|
||||
secondReceiptInfo.first { it.recipientId == harness.others[0] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED
|
||||
secondReceiptInfo.first { it.recipientId == harness.others[1] }.status assertIs GroupReceiptTable.STATUS_UNDELIVERED
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorT
|
||||
decryptedGroupState
|
||||
)
|
||||
|
||||
val groupRecipient = Recipient.externalGroupExact(group)
|
||||
val groupRecipient = Recipient.externalGroupExact(group!!)
|
||||
val threadForGroup = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
|
||||
|
||||
val insertResult = MmsHelper.insert(
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.messages
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import okio.ByteString
|
||||
@@ -58,14 +59,14 @@ class MessageProcessingPerformanceTest {
|
||||
mockkStatic(UnidentifiedAccessUtil::class)
|
||||
every { UnidentifiedAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
|
||||
|
||||
mockkStatic(MessageContentProcessor::class)
|
||||
every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application)
|
||||
mockkObject(MessageContentProcessorV2)
|
||||
every { MessageContentProcessorV2.create(harness.application) } returns TimingMessageContentProcessorV2(harness.application)
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
unmockkStatic(UnidentifiedAccessUtil::class)
|
||||
unmockkStatic(MessageContentProcessor::class)
|
||||
unmockkStatic(MessageContentProcessorV2::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -106,7 +107,7 @@ class MessageProcessingPerformanceTest {
|
||||
// Wait until they've all been fully decrypted + processed
|
||||
harness
|
||||
.inMemoryLogger
|
||||
.getLockForUntil(TimingMessageContentProcessor.endTagPredicate(lastTimestamp))
|
||||
.getLockForUntil(TimingMessageContentProcessorV2.endTagPredicate(lastTimestamp))
|
||||
.awaitFor(1.minutes)
|
||||
|
||||
harness.inMemoryLogger.flush()
|
||||
@@ -125,7 +126,7 @@ class MessageProcessingPerformanceTest {
|
||||
|
||||
// Calculate MessageContentProcessor
|
||||
|
||||
val takeLast: List<Entry> = entries.filter { it.tag == TimingMessageContentProcessor.TAG }.drop(2)
|
||||
val takeLast: List<Entry> = entries.filter { it.tag == TimingMessageContentProcessorV2.TAG }.drop(2)
|
||||
val iterator = takeLast.iterator()
|
||||
var processCount = 0L
|
||||
var processDuration = 0L
|
||||
@@ -141,7 +142,7 @@ class MessageProcessingPerformanceTest {
|
||||
// Calculate messages per second from "retrieving" first message post session initialization to processing last message
|
||||
|
||||
val start = entries.first { it.message == "Retrieved envelope! $firstTimestamp" }
|
||||
val end = entries.first { it.message == TimingMessageContentProcessor.endTag(lastTimestamp) }
|
||||
val end = entries.first { it.message == TimingMessageContentProcessorV2.endTag(lastTimestamp) }
|
||||
|
||||
val duration = (end.timestamp - start.timestamp).toFloat() / 1000f
|
||||
val messagePerSecond = messageCount.toFloat() / duration
|
||||
@@ -156,7 +157,7 @@ class MessageProcessingPerformanceTest {
|
||||
|
||||
val aliceProcessFirstMessageLatch = harness
|
||||
.inMemoryLogger
|
||||
.getLockForUntil(TimingMessageContentProcessor.endTagPredicate(firstPreKeyMessageTimestamp))
|
||||
.getLockForUntil(TimingMessageContentProcessorV2.endTagPredicate(firstPreKeyMessageTimestamp))
|
||||
|
||||
Thread { aliceClient.process(encryptedEnvelope, System.currentTimeMillis()) }.start()
|
||||
aliceProcessFirstMessageLatch.awaitFor(15.seconds)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
|
||||
data class TestMessage(
|
||||
val envelope: SignalServiceProtos.Envelope,
|
||||
val content: SignalServiceProtos.Content,
|
||||
val metadata: EnvelopeMetadata,
|
||||
val serverDeliveredTimestamp: Long
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.testing.LogPredicate
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
|
||||
class TimingMessageContentProcessor(context: Context) : MessageContentProcessor(context) {
|
||||
companion object {
|
||||
val TAG = Log.tag(TimingMessageContentProcessor::class.java)
|
||||
|
||||
fun endTagPredicate(timestamp: Long): LogPredicate = { entry ->
|
||||
entry.tag == TAG && entry.message == endTag(timestamp)
|
||||
}
|
||||
|
||||
private fun startTag(timestamp: Long) = "$timestamp start"
|
||||
fun endTag(timestamp: Long) = "$timestamp end"
|
||||
}
|
||||
|
||||
override fun process(messageState: MessageState?, content: SignalServiceContent?, exceptionMetadata: ExceptionMetadata?, envelopeTimestamp: Long, smsMessageId: Long) {
|
||||
Log.d(TAG, startTag(envelopeTimestamp))
|
||||
super.process(messageState, content, exceptionMetadata, envelopeTimestamp, smsMessageId)
|
||||
Log.d(TAG, endTag(envelopeTimestamp))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.testing.LogPredicate
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
|
||||
class TimingMessageContentProcessorV2(context: Context) : MessageContentProcessorV2(context) {
|
||||
companion object {
|
||||
val TAG = Log.tag(TimingMessageContentProcessorV2::class.java)
|
||||
|
||||
fun endTagPredicate(timestamp: Long): LogPredicate = { entry ->
|
||||
entry.tag == TAG && entry.message == endTag(timestamp)
|
||||
}
|
||||
|
||||
private fun startTag(timestamp: Long) = "$timestamp start"
|
||||
fun endTag(timestamp: Long) = "$timestamp end"
|
||||
}
|
||||
|
||||
override fun process(envelope: SignalServiceProtos.Envelope, content: SignalServiceProtos.Content, metadata: EnvelopeMetadata, serverDeliveredTimestamp: Long, processingEarlyContent: Boolean, localMetric: SignalLocalMetrics.MessageReceive?) {
|
||||
Log.d(TAG, startTag(envelope.timestamp))
|
||||
super.process(envelope, content, metadata, serverDeliveredTimestamp, processingEarlyContent, localMetric)
|
||||
Log.d(TAG, endTag(envelope.timestamp))
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.signal.libsignal.protocol.ecc.ECKeyPair
|
||||
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord
|
||||
import org.signal.libsignal.protocol.state.IdentityKeyStore
|
||||
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.PreKeyBundle
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SessionRecord
|
||||
@@ -31,6 +32,7 @@ import org.whispersystems.signalservice.api.push.DistributionId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
@@ -155,6 +157,11 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
override fun storeSignedPreKey(signedPreKeyId: Int, record: SignedPreKeyRecord?) = throw UnsupportedOperationException()
|
||||
override fun containsSignedPreKey(signedPreKeyId: Int): Boolean = throw UnsupportedOperationException()
|
||||
override fun removeSignedPreKey(signedPreKeyId: Int) = throw UnsupportedOperationException()
|
||||
override fun loadKyberPreKey(kyberPreKeyId: Int): KyberPreKeyRecord = throw UnsupportedOperationException()
|
||||
override fun loadKyberPreKeys(): MutableList<KyberPreKeyRecord> = throw UnsupportedOperationException()
|
||||
override fun storeKyberPreKey(kyberPreKeyId: Int, record: KyberPreKeyRecord?) = throw UnsupportedOperationException()
|
||||
override fun containsKyberPreKey(kyberPreKeyId: Int): Boolean = throw UnsupportedOperationException()
|
||||
override fun markKyberPreKeyUsed(kyberPreKeyId: Int) = throw UnsupportedOperationException()
|
||||
override fun 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()
|
||||
@@ -162,6 +169,10 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
|
||||
override fun getSenderKeySharedWith(distributionId: DistributionId?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
|
||||
override fun markSenderKeySharedWith(distributionId: DistributionId?, addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||
override fun clearSenderKeySharedWith(addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
|
||||
override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) = throw UnsupportedOperationException()
|
||||
override fun removeKyberPreKey(kyberPreKeyId: Int) = throw UnsupportedOperationException()
|
||||
override fun loadLastResortKyberPreKeys(): List<KyberPreKeyRecord> = throw UnsupportedOperationException()
|
||||
|
||||
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ object FakeClientHelpers {
|
||||
val selfUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(myProfileKey)
|
||||
val themUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
|
||||
|
||||
return UnidentifiedAccessPair(UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate.serialized), UnidentifiedAccess(themUnidentifiedAccessKey, senderCertificate.serialized)).targetUnidentifiedAccess
|
||||
return UnidentifiedAccessPair(UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate.serialized, false), UnidentifiedAccess(themUnidentifiedAccessKey, senderCertificate.serialized, false)).targetUnidentifiedAccess
|
||||
}
|
||||
|
||||
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {
|
||||
@@ -69,7 +69,7 @@ object FakeClientHelpers {
|
||||
.setSourceDevice(1)
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 1)
|
||||
.setDestinationUuid(destination.toString())
|
||||
.setDestinationServiceId(destination.toString())
|
||||
.setServerGuid(UUID.randomUUID().toString())
|
||||
.setContent(Base64.decode(this.content).toProtoByteString())
|
||||
.setUrgent(true)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.signal.storageservice.protos.groups.Member
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedMember
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Helper methods for creating groups for message processing tests et al.
|
||||
*/
|
||||
object GroupTestingUtils {
|
||||
fun member(serviceId: ServiceId, revision: Int = 0, role: Member.Role = Member.Role.ADMINISTRATOR): DecryptedMember {
|
||||
return DecryptedMember.newBuilder()
|
||||
.setUuid(serviceId.toByteString())
|
||||
.setJoinedAtRevision(revision)
|
||||
.setRole(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())
|
||||
.build()
|
||||
|
||||
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!!
|
||||
val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId)
|
||||
SignalDatabase.recipients.setProfileSharing(groupRecipientId, true)
|
||||
|
||||
return TestGroupInfo(groupId, groupMasterKey, groupRecipientId)
|
||||
}
|
||||
|
||||
fun RecipientId.asMember(): DecryptedMember {
|
||||
return Recipient.resolved(this).asMember()
|
||||
}
|
||||
|
||||
fun Recipient.asMember(): DecryptedMember {
|
||||
return member(serviceId = requireServiceId())
|
||||
}
|
||||
|
||||
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId)
|
||||
}
|
||||
@@ -30,6 +30,16 @@ class InMemoryLogger : Log.Logger() {
|
||||
latch.await()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
val latch = CountDownLatch(1)
|
||||
executor.execute {
|
||||
predicates.clear()
|
||||
logEntries.clear()
|
||||
latch.countDown()
|
||||
}
|
||||
latch.await()
|
||||
}
|
||||
|
||||
private fun add(entry: Entry) {
|
||||
executor.execute {
|
||||
logEntries += entry
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.database.model.toProtoByteString
|
||||
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 java.util.UUID
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
/**
|
||||
* Random but deterministic fuzzer for create various message content protos.
|
||||
*/
|
||||
object MessageContentFuzzer {
|
||||
|
||||
private val mediaTypes = listOf("image/png", "image/jpeg", "image/heic", "image/heif", "image/avif", "image/webp", "image/gif", "audio/aac", "audio/*", "video/mp4", "video/*", "text/x-vcard", "text/x-signal-plain", "application/x-signal-view-once", "*/*", "application/octet-stream")
|
||||
private val emojis = listOf("😂", "❤️", "🔥", "😍", "👀", "🤔", "🙏", "👍", "🤷", "🥺")
|
||||
|
||||
private val random = Random(1)
|
||||
|
||||
/**
|
||||
* Create an [Envelope].
|
||||
*/
|
||||
fun envelope(timestamp: Long): Envelope {
|
||||
return Envelope.newBuilder()
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(timestamp + 5)
|
||||
.setServerGuidBytes(UuidUtil.toByteString(UUID.randomUUID()))
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create metadata to match an [Envelope].
|
||||
*/
|
||||
fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata {
|
||||
return EnvelopeMetadata(
|
||||
sourceServiceId = Recipient.resolved(source).requireServiceId(),
|
||||
sourceE164 = null,
|
||||
sourceDeviceId = 1,
|
||||
sealedSender = true,
|
||||
groupId = groupId?.decodedId,
|
||||
destinationServiceId = Recipient.resolved(destination).requireServiceId()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random text message that will contain a body but may also contain
|
||||
* - An expire timer value
|
||||
* - Bold style body ranges
|
||||
*/
|
||||
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
if (groupContextV2 != null) {
|
||||
groupV2 = groupContextV2
|
||||
}
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sync sent text message for the given [DataMessage].
|
||||
*/
|
||||
fun syncSentTextMessage(
|
||||
textMessage: DataMessage,
|
||||
deliveredTo: List<RecipientId>,
|
||||
recipientUpdate: Boolean = false
|
||||
): Content {
|
||||
return Content
|
||||
.newBuilder()
|
||||
.setSyncMessage(
|
||||
SyncMessage.newBuilder().buildWith {
|
||||
sent = SyncMessage.Sent.newBuilder().buildWith {
|
||||
timestamp = textMessage.timestamp
|
||||
message = textMessage
|
||||
isRecipientUpdate = recipientUpdate
|
||||
addAllUnidentifiedStatus(
|
||||
deliveredTo.map {
|
||||
SyncMessage.Sent.UnidentifiedDeliveryStatus.newBuilder().buildWith {
|
||||
destinationServiceId = Recipient.resolved(it).requireServiceId().toString()
|
||||
unidentified = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that may be:
|
||||
* - A text body
|
||||
* - A text body with a quote that references an existing message
|
||||
* - A text body with a quote that references a non existing message
|
||||
* - 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 {
|
||||
if (random.nextBoolean()) {
|
||||
body = string()
|
||||
}
|
||||
|
||||
if (random.nextBoolean() && quoteAble.isNotEmpty()) {
|
||||
body = string()
|
||||
val quoted = quoteAble.random(random)
|
||||
quote = DataMessage.Quote.newBuilder().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)
|
||||
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)
|
||||
authorAci = quoted.metadata.sourceServiceId.toString()
|
||||
text = quoted.content.dataMessage.body
|
||||
}
|
||||
}
|
||||
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val total = random.nextInt(1, 2)
|
||||
(0..total).forEach { _ -> addAttachments(attachmentPointer()) }
|
||||
}
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a random media message that contains no traditional media content. It may be:
|
||||
* - A reaction to a prior message
|
||||
*/
|
||||
fun fuzzMediaMessageNoContent(previousMessages: List<TestMessage> = emptyList()): Content {
|
||||
return Content.newBuilder()
|
||||
.setDataMessage(
|
||||
DataMessage.newBuilder().buildWith {
|
||||
if (random.nextFloat() < 0.25) {
|
||||
val reactTo = previousMessages.random(random)
|
||||
reaction = DataMessage.Reaction.newBuilder().buildWith {
|
||||
emoji = emojis.random(random)
|
||||
remove = false
|
||||
targetAuthorAci = reactTo.metadata.sourceServiceId.toString()
|
||||
targetSentTimestamp = reactTo.envelope.timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a random media message that can never contain a text body. It may be:
|
||||
* - 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random [String].
|
||||
*/
|
||||
fun string(length: Int = 10, allowNullString: Boolean = false): String {
|
||||
var string = ""
|
||||
|
||||
if (allowNullString && random.nextBoolean()) {
|
||||
return string
|
||||
}
|
||||
|
||||
for (i in 0 until length) {
|
||||
string += random.nextInt(65..90).toChar()
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random [ByteString].
|
||||
*/
|
||||
fun byteString(length: Int = 512): ByteString {
|
||||
return random.nextBytes(length).toProtoByteString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a random [AttachmentPointer].
|
||||
*/
|
||||
fun attachmentPointer(): AttachmentPointer {
|
||||
return AttachmentPointer.newBuilder().run {
|
||||
cdnKey = string()
|
||||
contentType = mediaTypes.random(random)
|
||||
key = byteString()
|
||||
size = random.nextInt(1024 * 1024 * 50)
|
||||
thumbnail = byteString()
|
||||
digest = byteString()
|
||||
fileName = string()
|
||||
flags = 0
|
||||
width = random.nextInt(until = 1024)
|
||||
height = random.nextInt(until = 1024)
|
||||
caption = string(allowNullString = true)
|
||||
blurHash = string()
|
||||
uploadTimestamp = random.nextLong()
|
||||
cdnNumber = 1
|
||||
|
||||
build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a server delivered timestamp that is always later than the envelope and server "received" timestamp.
|
||||
*/
|
||||
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
|
||||
return envelopeTimestamp + 10
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
package org.thoughtcrime.securesms.testing
|
||||
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import org.mockito.kotlin.any
|
||||
import org.mockito.kotlin.anyOrNull
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.stub
|
||||
import org.signal.core.util.Hex
|
||||
import org.signal.libsignal.protocol.IdentityKeyPair
|
||||
@@ -11,21 +9,17 @@ 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.pin.KbsRepository
|
||||
import org.thoughtcrime.securesms.pin.TokenData
|
||||
import org.thoughtcrime.securesms.test.BuildConfig
|
||||
import org.whispersystems.signalservice.api.KbsPinData
|
||||
import org.whispersystems.signalservice.api.KeyBackupService
|
||||
import org.whispersystems.signalservice.api.kbs.HashedPin
|
||||
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
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials
|
||||
import org.whispersystems.signalservice.internal.push.DeviceInfoList
|
||||
import org.whispersystems.signalservice.internal.push.PreKeyEntity
|
||||
@@ -46,7 +40,8 @@ object MockProvider {
|
||||
val senderCertificate = SenderCertificate().apply { certificate = ByteArray(0) }
|
||||
|
||||
val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply {
|
||||
backupCredentials = AuthCredentials.create("username", "password")
|
||||
svr1Credentials = AuthCredentials.create("username", "password")
|
||||
svr2Credentials = null
|
||||
}
|
||||
|
||||
val primaryOnlyDeviceList = DeviceInfoList().apply {
|
||||
@@ -83,26 +78,15 @@ object MockProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fun mockGetRegistrationLockStringFlow(kbsRepository: KbsRepository) {
|
||||
val tokenData: TokenData = mock {
|
||||
on { enclave } doReturn BuildConfig.KBS_ENCLAVE
|
||||
on { basicAuth } doReturn "basicAuth"
|
||||
on { triesRemaining } doReturn 10
|
||||
on { tokenResponse } doReturn TokenResponse()
|
||||
}
|
||||
|
||||
kbsRepository.stub {
|
||||
on { getToken(any() as? String) } doReturn Single.just(ServiceResponse.forResult(tokenData, 200, ""))
|
||||
}
|
||||
|
||||
fun mockGetRegistrationLockStringFlow() {
|
||||
val session: KeyBackupService.RestoreSession = object : KeyBackupService.RestoreSession {
|
||||
override fun hashSalt(): ByteArray = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8")
|
||||
override fun restorePin(hashedPin: HashedPin?): KbsPinData = KbsPinData(MasterKey.createNew(SecureRandom()), null)
|
||||
override fun restorePin(hashedPin: PinHash?): SvrPinData = SvrPinData(MasterKey.createNew(SecureRandom()), null)
|
||||
}
|
||||
|
||||
val kbsService = ApplicationDependencies.getKeyBackupService(BuildConfig.KBS_ENCLAVE)
|
||||
kbsService.stub {
|
||||
on { newRegistrationSession(any(), any()) } doReturn session
|
||||
on { newRegistrationSession(anyOrNull(), anyOrNull()) } doReturn session
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ class Put(path: String, responseFactory: ResponseFactory) : Verb(defaultRequestP
|
||||
|
||||
class Post(path: String, responseFactory: ResponseFactory) : Verb(defaultRequestPredicate("POST", path), responseFactory)
|
||||
|
||||
class Delete(path: String, responseFactory: ResponseFactory) : Verb(defaultRequestPredicate("DELETE", path), responseFactory)
|
||||
|
||||
fun MockResponse.success(response: Any? = null): MockResponse {
|
||||
return setResponseCode(200).apply {
|
||||
if (response != null) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.registration.RegistrationData
|
||||
import org.thoughtcrime.securesms.registration.RegistrationRepository
|
||||
import org.thoughtcrime.securesms.registration.RegistrationUtil
|
||||
import org.thoughtcrime.securesms.registration.VerifyResponse
|
||||
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
@@ -36,7 +37,6 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
|
||||
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
@@ -45,7 +45,7 @@ import java.util.UUID
|
||||
*
|
||||
* To use: `@get:Rule val harness = SignalActivityRule()`
|
||||
*/
|
||||
class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() {
|
||||
class SignalActivityRule(private val othersCount: Int = 4, private val createGroup: Boolean = false) : ExternalResource() {
|
||||
|
||||
val application: Application = ApplicationDependencies.getApplication()
|
||||
|
||||
@@ -57,6 +57,9 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
|
||||
private set
|
||||
lateinit var othersKeys: List<IdentityKeyPair>
|
||||
|
||||
var group: GroupTestingUtils.TestGroupInfo? = null
|
||||
private set
|
||||
|
||||
val inMemoryLogger: InMemoryLogger
|
||||
get() = (application as SignalInstrumentationApplicationContext).inMemoryLogger
|
||||
|
||||
@@ -68,6 +71,15 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
|
||||
others = setupOthers.first
|
||||
othersKeys = setupOthers.second
|
||||
|
||||
if (createGroup && others.size >= 2) {
|
||||
group = GroupTestingUtils.insertGroup(
|
||||
revision = 0,
|
||||
self.asMember(),
|
||||
others[0].asMember(),
|
||||
others[1].asMember()
|
||||
)
|
||||
}
|
||||
|
||||
InstrumentationApplicationDependencyProvider.clearHandlers()
|
||||
}
|
||||
|
||||
@@ -78,6 +90,9 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
|
||||
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
||||
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
||||
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
|
||||
val registrationRepository = RegistrationRepository(application)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(Put("/v2/keys") { MockResponse().success() })
|
||||
@@ -92,16 +107,24 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
|
||||
pniRegistrationId = registrationRepository.pniRegistrationId,
|
||||
recoveryPassword = "asdfasdfasdfasdf"
|
||||
),
|
||||
VerifyResponse(VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false), null, null),
|
||||
VerifyResponse(
|
||||
verifyAccountResponse = VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
|
||||
masterKey = null,
|
||||
pin = null,
|
||||
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
|
||||
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
|
||||
),
|
||||
false
|
||||
).blockingGet()
|
||||
|
||||
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
||||
|
||||
SignalStore.kbsValues().optOut()
|
||||
SignalStore.svr().optOut()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||
|
||||
SignalStore.settings().isMessageNotificationsEnabled = false
|
||||
|
||||
return Recipient.self()
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class SignalDatabaseRule(
|
||||
|
||||
private fun deleteAllThreads() {
|
||||
if (deleteAllThreadsOnEachRun) {
|
||||
SignalDatabase.messages.deleteAllThreads()
|
||||
SignalDatabase.threads.clearForTests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class TestProtos private constructor() {
|
||||
authorUuid: String = UUID.randomUUID().toString()
|
||||
): DataMessage.StoryContext.Builder {
|
||||
return DataMessage.StoryContext.newBuilder()
|
||||
.setAuthorUuid(authorUuid)
|
||||
.setAuthorAci(authorUuid)
|
||||
.setSentTimestamp(sentTimestamp)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ fun <T : Any?> T.assertIsNotNull() {
|
||||
assertThat(this, notNullValue())
|
||||
}
|
||||
|
||||
infix fun <T : Any> T.assertIs(expected: T) {
|
||||
infix fun <T : Any?> T.assertIs(expected: T) {
|
||||
assertThat(this, `is`(expected))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
|
||||
/**
|
||||
* Helper methods for interacting with [MessageTable] in tests.
|
||||
*/
|
||||
object MessageTableTestUtils {
|
||||
|
||||
fun getMessages(threadId: Long): List<MessageRecord> {
|
||||
return MessageTable.mmsReaderFor(SignalDatabase.messages.getConversation(threadId)).use {
|
||||
it.toList()
|
||||
}
|
||||
}
|
||||
|
||||
fun typeColumnToString(type: Long): String {
|
||||
return """
|
||||
isOutgoingMessageType:${MessageTypes.isOutgoingMessageType(type)}
|
||||
isForcedSms:${type and MessageTypes.MESSAGE_FORCE_SMS_BIT != 0L}
|
||||
isDraftMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_DRAFT_TYPE}
|
||||
isFailedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_FAILED_TYPE}
|
||||
isPendingMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_OUTBOX_TYPE || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENDING_TYPE}
|
||||
isSentType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_SENT_TYPE}
|
||||
isPendingSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK || type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
|
||||
isPendingSecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_SECURE_SMS_FALLBACK}
|
||||
isPendingInsecureSmsFallbackType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_PENDING_INSECURE_SMS_FALLBACK}
|
||||
isInboxType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BASE_INBOX_TYPE}
|
||||
isJoinedType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.JOINED_TYPE}
|
||||
isUnsupportedMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.UNSUPPORTED_MESSAGE_TYPE}
|
||||
isInvalidMessageType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.INVALID_MESSAGE_TYPE}
|
||||
isBadDecryptType:${type and MessageTypes.BASE_TYPE_MASK == MessageTypes.BAD_DECRYPT_TYPE}
|
||||
isSecureType:${type and MessageTypes.SECURE_MESSAGE_BIT != 0L}
|
||||
isPushType:${type and MessageTypes.PUSH_MESSAGE_BIT != 0L}
|
||||
isEndSessionType:${type and MessageTypes.END_SESSION_BIT != 0L}
|
||||
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}
|
||||
isIncomingAudioCall:${type == MessageTypes.INCOMING_AUDIO_CALL_TYPE}
|
||||
isIncomingVideoCall:${type == MessageTypes.INCOMING_VIDEO_CALL_TYPE}
|
||||
isOutgoingAudioCall:${type == MessageTypes.OUTGOING_AUDIO_CALL_TYPE}
|
||||
isOutgoingVideoCall:${type == MessageTypes.OUTGOING_VIDEO_CALL_TYPE}
|
||||
isMissedAudioCall:${type == MessageTypes.MISSED_AUDIO_CALL_TYPE}
|
||||
isMissedVideoCall:${type == MessageTypes.MISSED_VIDEO_CALL_TYPE}
|
||||
isGroupCall:${type == MessageTypes.GROUP_CALL_TYPE}
|
||||
isGroupUpdate:${type and MessageTypes.GROUP_UPDATE_BIT != 0L}
|
||||
isGroupV2:${type and MessageTypes.GROUP_V2_BIT != 0L}
|
||||
isGroupQuit:${type and MessageTypes.GROUP_LEAVE_BIT != 0L && type and MessageTypes.GROUP_V2_BIT == 0L}
|
||||
isChatSessionRefresh:${type and MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT != 0L}
|
||||
isDuplicateMessageType:${type and MessageTypes.ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L}
|
||||
isDecryptInProgressType:${type and 0x40000000 != 0L}
|
||||
isNoRemoteSessionType:${type and MessageTypes.ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L}
|
||||
isLegacyType:${type and MessageTypes.ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and MessageTypes.ENCRYPTION_REMOTE_BIT != 0L}
|
||||
isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE}
|
||||
isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE}
|
||||
isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE}
|
||||
isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE}
|
||||
isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE}
|
||||
isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE}
|
||||
isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS}
|
||||
isSpecialType:${type and MessageTypes.SPECIAL_TYPES_MASK != 0L}
|
||||
isStoryReaction:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_STORY_REACTION}
|
||||
isGiftBadge:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_GIFT_BADGE}
|
||||
isPaymentsNotificaiton:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION}
|
||||
isRequestToActivatePayments:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST}
|
||||
isPaymentsActivated:${type and MessageTypes.SPECIAL_TYPES_MASK == MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED}
|
||||
""".trimIndent().replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "")
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,6 @@ import org.signal.benchmark.setup.TestMessages
|
||||
import org.signal.benchmark.setup.TestUsers
|
||||
import org.thoughtcrime.securesms.BaseActivity
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
class BenchmarkSetupActivity : BaseActivity() {
|
||||
@@ -53,13 +51,6 @@ class BenchmarkSetupActivity : BaseActivity() {
|
||||
TestMessages.insertOutgoingTextMessage(other = recipient, body = "Test message $i", timestamp = generator.nextTimestamp())
|
||||
}
|
||||
|
||||
val voiceMessageId = TestMessages.insertIncomingVoiceMessage(other = recipient, timestamp = generator.nextTimestamp())
|
||||
val mmsRecord = SignalDatabase.messages.getMessageRecord(voiceMessageId) as MediaMmsMessageRecord
|
||||
TestMessages.insertOutgoingImageMessage(other = recipient, body = "test", 2, generator.nextTimestamp())
|
||||
TestMessages.insertIncomingTextMessage(other = recipient, "reply to the test message", generator.nextTimestamp())
|
||||
TestMessages.insertIncomingQuoteTextMessage(other = recipient, quote = QuoteModel(mmsRecord.timestamp, recipient.id, "Fake voice message text", false, mmsRecord.slideDeck.asAttachments(), null, QuoteModel.Type.NORMAL, null), body = "Here is a cool quote", timestamp = generator.nextTimestamp())
|
||||
TestMessages.insertOutgoingTextMessage(other = recipient, body = "longaweorijoaijwerijoiajwer", timestamp = generator.nextTimestamp())
|
||||
|
||||
SignalDatabase.threads.update(SignalDatabase.threads.getOrCreateThreadIdFor(recipient = recipient), true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package org.signal.benchmark
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.libsignal.protocol.IdentityKey
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.push.AccountManagerFactory
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager
|
||||
import org.whispersystems.signalservice.api.account.PreKeyUpload
|
||||
import org.whispersystems.signalservice.api.push.ACI
|
||||
import org.whispersystems.signalservice.api.push.PNI
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
|
||||
import java.io.IOException
|
||||
import java.util.Optional
|
||||
@@ -37,7 +34,7 @@ class DummyAccountManagerFactory : AccountManagerFactory() {
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun setPreKeys(serviceIdType: ServiceIdType, identityKey: IdentityKey, signedPreKey: SignedPreKeyRecord, oneTimePreKeys: List<PreKeyRecord>) {
|
||||
override fun setPreKeys(preKeyUpload: PreKeyUpload) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +148,7 @@ object TestMessages {
|
||||
1024,
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of("/not-there.jpg"),
|
||||
false,
|
||||
false,
|
||||
@@ -169,6 +170,7 @@ object TestMessages {
|
||||
1024,
|
||||
1024,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of("/not-there.aac"),
|
||||
true,
|
||||
false,
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
import org.signal.benchmark.DummyAccountManagerFactory
|
||||
import org.signal.core.util.concurrent.safeBlockingGet
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
|
||||
@@ -43,6 +44,9 @@ object TestUsers {
|
||||
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
|
||||
preferences.edit().putBoolean("passphrase_initialized", true).commit()
|
||||
|
||||
SignalStore.account().generateAciIdentityKeyIfNecessary()
|
||||
SignalStore.account().generatePniIdentityKeyIfNecessary()
|
||||
|
||||
val registrationRepository = RegistrationRepository(application)
|
||||
val registrationData = RegistrationData(
|
||||
code = "123123",
|
||||
@@ -54,16 +58,26 @@ object TestUsers {
|
||||
pniRegistrationId = registrationRepository.pniRegistrationId,
|
||||
recoveryPassword = "asdfasdfasdfasdf"
|
||||
)
|
||||
val verifyResponse = VerifyResponse(VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false), null, null)
|
||||
|
||||
val verifyResponse = VerifyResponse(
|
||||
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
|
||||
masterKey = null,
|
||||
pin = null,
|
||||
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
|
||||
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
|
||||
)
|
||||
|
||||
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
|
||||
|
||||
val response: ServiceResponse<VerifyResponse> = registrationRepository.registerAccount(
|
||||
registrationData,
|
||||
verifyResponse,
|
||||
false
|
||||
).blockingGet()
|
||||
).safeBlockingGet()
|
||||
|
||||
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
|
||||
|
||||
SignalStore.kbsValues().optOut()
|
||||
SignalStore.svr().optOut()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
|
||||
|
||||
|
||||
9
app/src/canary/AndroidManifest.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:name=".CanaryApplicationContext"
|
||||
tools:replace="android:name" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.thoughtcrime.securesms
|
||||
|
||||
import android.os.StrictMode
|
||||
import android.os.StrictMode.ThreadPolicy
|
||||
import leakcanary.LeakCanary
|
||||
import shark.AndroidReferenceMatchers
|
||||
|
||||
class CanaryApplicationContext : ApplicationContext() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
StrictMode.setThreadPolicy(
|
||||
ThreadPolicy.Builder()
|
||||
.detectDiskReads()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.build()
|
||||
)
|
||||
|
||||
try {
|
||||
Class.forName("dalvik.system.CloseGuard")
|
||||
.getMethod("setEnabled", Boolean::class.javaPrimitiveType)
|
||||
.invoke(null, true)
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
referenceMatchers = AndroidReferenceMatchers.appDefaults +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.service.media.MediaBrowserService\$ServiceBinder",
|
||||
fieldName = "this\$0"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "androidx.media.MediaBrowserServiceCompat\$MediaBrowserServiceImplApi26\$MediaBrowserServiceApi26",
|
||||
fieldName = "mBase"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.MediaBrowserCompat",
|
||||
fieldName = "mImpl"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||
fieldName = "mToken"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "android.support.v4.media.session.MediaControllerCompat",
|
||||
fieldName = "mImpl"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService",
|
||||
fieldName = "mApplication"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.service.GenericForegroundService\$LocalBinder",
|
||||
fieldName = "this\$0"
|
||||
) +
|
||||
AndroidReferenceMatchers.ignoredInstanceField(
|
||||
className = "org.thoughtcrime.securesms.contacts.ContactsSyncAdapter",
|
||||
fieldName = "mContext"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation.springboard
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import org.signal.core.ui.Rows
|
||||
import org.signal.core.ui.Scaffolds
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
|
||||
/**
|
||||
* Configuration fragment for the internal conversation test fragment.
|
||||
*/
|
||||
class InternalConversationSpringboardFragment : ComposeFragment() {
|
||||
|
||||
private val viewModel: InternalConversationSpringboardViewModel by navGraphViewModels(R.id.app_settings)
|
||||
|
||||
@Composable
|
||||
override fun FragmentContent() {
|
||||
Content(this::navigateBack, this::launchTestFragment, viewModel.hasWallpaper)
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
private fun launchTestFragment() {
|
||||
findNavController().navigate(
|
||||
InternalConversationSpringboardFragmentDirections
|
||||
.actionInternalConversationSpringboardFragmentToInternalConversationTestFragment()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ContentPreview() {
|
||||
val hasWallpaper = remember { mutableStateOf(false) }
|
||||
|
||||
SignalTheme(isDarkMode = true) {
|
||||
Content(onBackPressed = {}, onLaunchTestFragment = {}, hasWallpaper = hasWallpaper)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
onBackPressed: () -> Unit,
|
||||
onLaunchTestFragment: () -> Unit,
|
||||
hasWallpaper: MutableState<Boolean>
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "Conversation Test Springboard",
|
||||
onNavigationClick = onBackPressed,
|
||||
navigationIconPainter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.symbol_arrow_left_24))
|
||||
) {
|
||||
Column(modifier = Modifier.padding(it)) {
|
||||
Rows.TextRow(
|
||||
text = "Launch Conversation Test Fragment",
|
||||
onClick = onLaunchTestFragment
|
||||
)
|
||||
|
||||
Rows.ToggleRow(
|
||||
checked = hasWallpaper.value,
|
||||
text = "Enable Wallpaper",
|
||||
onCheckChanged = { hasWallpaper.value = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation.springboard
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class InternalConversationSpringboardViewModel : ViewModel() {
|
||||
val hasWallpaper = mutableStateOf(false)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation.test
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
|
||||
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.StoryType
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import java.security.SecureRandom
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
/**
|
||||
* Generates random conversation messages via the given set of parameters.
|
||||
*/
|
||||
class ConversationElementGenerator {
|
||||
private val mappingModelCache = mutableMapOf<ConversationElementKey, MappingModel<*>>()
|
||||
private val random = SecureRandom()
|
||||
|
||||
private val wordBank = listOf(
|
||||
"A",
|
||||
"Test",
|
||||
"Message",
|
||||
"To",
|
||||
"Display",
|
||||
"Content",
|
||||
"In",
|
||||
"Bubbles",
|
||||
"User",
|
||||
"Signal",
|
||||
"The"
|
||||
)
|
||||
|
||||
fun getMappingModel(key: ConversationElementKey): MappingModel<*> {
|
||||
val cached = mappingModelCache[key]
|
||||
if (cached != null) {
|
||||
return cached
|
||||
}
|
||||
|
||||
val messageModel = generateMessage(key)
|
||||
mappingModelCache[key] = messageModel
|
||||
return messageModel
|
||||
}
|
||||
|
||||
private fun getIncomingType(): Long {
|
||||
return MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT
|
||||
}
|
||||
|
||||
private fun getSentOutgoingType(): Long {
|
||||
return MessageTypes.BASE_SENT_TYPE or MessageTypes.SECURE_MESSAGE_BIT
|
||||
}
|
||||
|
||||
private fun getSentFailedOutgoingType(): Long {
|
||||
return MessageTypes.BASE_SENT_FAILED_TYPE or MessageTypes.SECURE_MESSAGE_BIT
|
||||
}
|
||||
|
||||
private fun getPendingOutgoingType(): Long {
|
||||
return MessageTypes.BASE_OUTBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT
|
||||
}
|
||||
|
||||
private fun generateMessage(key: ConversationElementKey): MappingModel<*> {
|
||||
val messageId = key.requireMessageId()
|
||||
val now = getNow()
|
||||
|
||||
val testMessageWordLength = random.nextInt(3) + 1
|
||||
val testMessage = (0 until testMessageWordLength).map {
|
||||
wordBank.random()
|
||||
}.joinToString(" ")
|
||||
|
||||
val isIncoming = random.nextBoolean()
|
||||
|
||||
val record = MediaMmsMessageRecord(
|
||||
messageId,
|
||||
if (isIncoming) Recipient.UNKNOWN else Recipient.self(),
|
||||
0,
|
||||
if (isIncoming) Recipient.self() else Recipient.UNKNOWN,
|
||||
now,
|
||||
now,
|
||||
now,
|
||||
1,
|
||||
1,
|
||||
testMessage,
|
||||
SlideDeck(),
|
||||
if (isIncoming) getIncomingType() else getPendingOutgoingType(),
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
1,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
false,
|
||||
emptyList(),
|
||||
false,
|
||||
false,
|
||||
now,
|
||||
1,
|
||||
now,
|
||||
null,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
null,
|
||||
null,
|
||||
0
|
||||
)
|
||||
|
||||
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
|
||||
ApplicationDependencies.getApplication(),
|
||||
record,
|
||||
Recipient.UNKNOWN
|
||||
)
|
||||
|
||||
return if (isIncoming) {
|
||||
IncomingTextOnly(conversationMessage)
|
||||
} else {
|
||||
OutgoingTextOnly(conversationMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNow(): Long {
|
||||
val now = System.currentTimeMillis()
|
||||
return now - random.nextInt(20.milliseconds.inWholeMilliseconds.toInt()).toLong()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation.test
|
||||
|
||||
import org.signal.paging.PagedDataSource
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import kotlin.math.min
|
||||
|
||||
class InternalConversationTestDataSource(
|
||||
private val size: Int,
|
||||
private val generator: ConversationElementGenerator
|
||||
) : PagedDataSource<ConversationElementKey, MappingModel<*>> {
|
||||
override fun size(): Int = size
|
||||
|
||||
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<MappingModel<*>> {
|
||||
val end = min(start + length, totalSize)
|
||||
return (start until end).map {
|
||||
load(ConversationElementKey.forMessage(it.toLong()))!!
|
||||
}.toMutableList()
|
||||
}
|
||||
|
||||
override fun getKey(data: MappingModel<*>): ConversationElementKey {
|
||||
check(data is ConversationMessageElement)
|
||||
|
||||
return ConversationElementKey.forMessage(data.conversationMessage.messageRecord.id)
|
||||
}
|
||||
|
||||
override fun load(key: ConversationElementKey?): MappingModel<*>? {
|
||||
return key?.let { generator.getMappingModel(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation.test
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.navGraphViewModels
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.ringrtc.CallLinkRootKey
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.conversation.springboard.InternalConversationSpringboardViewModel
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
|
||||
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.database.model.InMemoryMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.databinding.ConversationTestFragmentBinding
|
||||
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
|
||||
import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||
|
||||
class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fragment) {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(InternalConversationTestFragment::class.java)
|
||||
}
|
||||
|
||||
private val binding by ViewBinderDelegate(ConversationTestFragmentBinding::bind)
|
||||
private val viewModel: InternalConversationTestViewModel by viewModels()
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private val springboardViewModel: InternalConversationSpringboardViewModel by navGraphViewModels(R.id.app_settings)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val adapter = ConversationAdapterV2(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
glideRequests = GlideApp.with(this),
|
||||
clickListener = ClickListener(),
|
||||
hasWallpaper = springboardViewModel.hasWallpaper.value,
|
||||
colorizer = Colorizer(),
|
||||
startExpirationTimeout = {}
|
||||
)
|
||||
|
||||
if (springboardViewModel.hasWallpaper.value) {
|
||||
binding.root.setBackgroundColor(0xFF32C7E2.toInt())
|
||||
}
|
||||
|
||||
var startTime = 0L
|
||||
var firstRender = true
|
||||
lifecycleDisposable.bindTo(viewLifecycleOwner)
|
||||
adapter.setPagingController(viewModel.controller)
|
||||
lifecycleDisposable += viewModel.data.observeOn(AndroidSchedulers.mainThread()).subscribeBy {
|
||||
if (firstRender) {
|
||||
startTime = System.currentTimeMillis()
|
||||
}
|
||||
adapter.submitList(it) {
|
||||
if (firstRender) {
|
||||
firstRender = false
|
||||
binding.root.doAfterNextLayout {
|
||||
val endTime = System.currentTimeMillis()
|
||||
Log.d(TAG, "First render in ${endTime - startTime} millis")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.recycler.layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
RecyclerViewColorizer(binding.recycler).apply {
|
||||
setChatColors(ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class ClickListener : ItemClickListener {
|
||||
override fun onQuoteClicked(messageRecord: MmsMessageRecord?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onLinkPreviewClicked(linkPreview: LinkPreview) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onStickerClicked(stickerLocator: StickerLocator) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onViewOnceMessageClicked(messageRecord: MmsMessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onSharedContactDetailsClicked(contact: Contact, avatarTransitionView: View) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onAddToContactsClicked(contact: Contact) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onMessageSharedContactClicked(choices: MutableList<Recipient>) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInviteSharedContactClicked(choices: MutableList<Recipient>) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onMessageWithErrorClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onIncomingIdentityMismatchClicked(recipientId: RecipientId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onRegisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onVoiceNotePause(uri: Uri) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onVoiceNotePlay(uri: Uri, messageId: Long, position: Double) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onVoiceNoteSeekTo(uri: Uri, position: Double) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onVoiceNotePlaybackSpeedChanged(uri: Uri, speed: Float) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onChatSessionRefreshLearnMoreClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onBadDecryptLearnMoreClicked(author: RecipientId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onJoinGroupCallClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onEnableCallNotificationsClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onPlayInlineContent(conversationMessage: ConversationMessage?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onChangeNumberUpdateContact(recipient: Recipient) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onCallToAction(action: String) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onDonateClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onBlockJoinRequest(recipient: Recipient) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onRecipientNameClicked(target: RecipientId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onInviteToSignalClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onActivatePaymentsClicked() {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onUrlClicked(url: String): Boolean {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onGiftBadgeRevealed(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MultiselectPart?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
|
||||
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.internal.conversation.test
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.signal.paging.PagedData
|
||||
import org.signal.paging.PagingConfig
|
||||
|
||||
class InternalConversationTestViewModel : ViewModel() {
|
||||
private val generator = ConversationElementGenerator()
|
||||
private val dataSource = InternalConversationTestDataSource(
|
||||
500,
|
||||
generator
|
||||
)
|
||||
|
||||
private val config = PagingConfig.Builder().setPageSize(25)
|
||||
.setBufferPages(2)
|
||||
.build()
|
||||
|
||||
private val pagedData = PagedData.createForObservable(dataSource, config)
|
||||
|
||||
val controller = pagedData.controller
|
||||
val data = pagedData.data
|
||||
}
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 238 KiB |
BIN
app/src/main/assets/emoji/Objects_0.webp
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
app/src/main/assets/emoji/Objects_1.webp
Normal file
|
After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 200 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 92 KiB |
BIN
app/src/main/assets/fonts/Hatsuishi-Regular.otf
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.signal.glide.transforms
|
||||
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object SignalDownsampleStrategy {
|
||||
/**
|
||||
* Center outside, but don't up-scale, only downscale. You should be setting centerOutside
|
||||
* on the target image view to still maintain center outside behavior.
|
||||
*/
|
||||
@JvmField
|
||||
val CENTER_OUTSIDE_NO_UPSCALE: DownsampleStrategy = CenterOutsideNoUpscale()
|
||||
|
||||
private class CenterOutsideNoUpscale : DownsampleStrategy() {
|
||||
override fun getScaleFactor(
|
||||
sourceWidth: Int,
|
||||
sourceHeight: Int,
|
||||
requestedWidth: Int,
|
||||
requestedHeight: Int
|
||||
): Float {
|
||||
val widthPercentage = requestedWidth / sourceWidth.toFloat()
|
||||
val heightPercentage = requestedHeight / sourceHeight.toFloat()
|
||||
return min(MAX_SCALE_FACTOR, max(widthPercentage, heightPercentage))
|
||||
}
|
||||
|
||||
override fun getSampleSizeRounding(
|
||||
sourceWidth: Int,
|
||||
sourceHeight: Int,
|
||||
requestedWidth: Int,
|
||||
requestedHeight: Int
|
||||
): SampleSizeRounding {
|
||||
return SampleSizeRounding.QUALITY
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_SCALE_FACTOR = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,7 @@ object AppCapabilities {
|
||||
@JvmStatic
|
||||
fun getCapabilities(storageCapable: Boolean): AccountAttributes.Capabilities {
|
||||
return AccountAttributes.Capabilities(
|
||||
uuid = false,
|
||||
gv2 = true,
|
||||
storage = storageCapable,
|
||||
gv1Migration = true,
|
||||
senderKey = true,
|
||||
announcementGroup = true,
|
||||
changeNumber = true,
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
@@ -31,12 +30,11 @@ import com.google.android.gms.security.ProviderInstaller;
|
||||
import org.conscrypt.Conscrypt;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.aesgcmprovider.AesGcmProvider;
|
||||
import org.signal.core.util.MemoryTracker;
|
||||
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.tracing.Tracer;
|
||||
import org.signal.donations.GooglePayApi;
|
||||
import org.signal.donations.StripeApi;
|
||||
import org.signal.glide.SignalGlideCodecs;
|
||||
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
@@ -50,7 +48,9 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.gcm.FcmFetchManager;
|
||||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
|
||||
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
@@ -62,7 +62,7 @@ import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshKbsCredentialsJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshSvrCredentialsJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
|
||||
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
|
||||
@@ -92,7 +92,6 @@ 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.Environment;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
|
||||
@@ -147,8 +146,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
|
||||
super.onCreate();
|
||||
|
||||
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
|
||||
.addBlocking("sqlcipher-init", () -> {
|
||||
AppStartup.getInstance().addBlocking("sqlcipher-init", () -> {
|
||||
SqlCipherLibraryLoader.load();
|
||||
SignalDatabase.init(this,
|
||||
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
|
||||
@@ -158,6 +156,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
initializeLogging();
|
||||
Log.i(TAG, "onCreate()");
|
||||
})
|
||||
.addBlocking("security-provider", this::initializeSecurityProvider)
|
||||
.addBlocking("crash-handling", this::initializeCrashHandling)
|
||||
.addBlocking("rx-init", this::initializeRx)
|
||||
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
|
||||
@@ -179,7 +178,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addBlocking("ring-rtc", this::initializeRingRtc)
|
||||
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
|
||||
.addNonBlocking(() -> GlideApp.get(this))
|
||||
.addNonBlocking(this::checkIsGooglePayReady)
|
||||
.addNonBlocking(this::cleanAvatarStorage)
|
||||
.addNonBlocking(this::initializeRevealableMessageManager)
|
||||
.addNonBlocking(this::initializePendingRetryReceiptManager)
|
||||
@@ -197,11 +195,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
|
||||
.addNonBlocking(this::ensureProfileUploaded)
|
||||
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
|
||||
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
|
||||
.addPostRender(this::initializeExpiringMessageManager)
|
||||
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
|
||||
.addPostRender(this::initializeTrimThreadsByDateManager)
|
||||
.addPostRender(RefreshKbsCredentialsJob::enqueueIfNecessary)
|
||||
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
|
||||
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
|
||||
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
|
||||
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
|
||||
@@ -214,8 +213,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
|
||||
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
|
||||
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
|
||||
.addPostRender(() -> SignalDatabase.groupCallRings().removeOldRings())
|
||||
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
|
||||
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
@@ -232,6 +231,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
|
||||
ApplicationDependencies.getDeadlockDetector().start();
|
||||
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
|
||||
FcmFetchManager.onForeground(this);
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
FeatureFlags.refreshIfNecessary();
|
||||
@@ -240,6 +240,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
KeyCachingService.onAppForegrounded(this);
|
||||
ApplicationDependencies.getShakeToReport().enable();
|
||||
checkBuildExpiration();
|
||||
MemoryTracker.start();
|
||||
|
||||
long lastForegroundTime = SignalStore.misc().getLastForegroundTime();
|
||||
long currentTime = System.currentTimeMillis();
|
||||
@@ -263,6 +264,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
ApplicationDependencies.getFrameRateTracker().stop();
|
||||
ApplicationDependencies.getShakeToReport().disable();
|
||||
ApplicationDependencies.getDeadlockDetector().stop();
|
||||
MemoryTracker.stop();
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
@@ -277,13 +279,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
}
|
||||
|
||||
private void initializeSecurityProvider() {
|
||||
try {
|
||||
Class.forName("org.signal.aesgcmprovider.AesGcmCipher");
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.e(TAG, "Failed to find AesGcmCipher class");
|
||||
throw new ProviderInitializationException();
|
||||
}
|
||||
|
||||
int aesPosition = Security.insertProviderAt(new AesGcmProvider(), 1);
|
||||
Log.i(TAG, "Installed AesGcmProvider: " + aesPosition);
|
||||
|
||||
@@ -476,25 +471,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
||||
AvatarPickerStorage.cleanOrphans(this);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void checkIsGooglePayReady() {
|
||||
GooglePayApi.queryIsReadyToPay(
|
||||
this,
|
||||
new StripeApi.Gateway(Environment.Donations.getStripeConfiguration()),
|
||||
Environment.Donations.getGooglePayConfiguration()
|
||||
).subscribe(
|
||||
/* onComplete = */ () -> SignalStore.donationsValues().setGooglePayReady(true),
|
||||
/* onError = */ t -> SignalStore.donationsValues().setGooglePayReady(false)
|
||||
);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private void initializeCleanup() {
|
||||
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
|
||||
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
|
||||
if (SignalStore.account().clearOldAccountDataReport()) {
|
||||
Log.i(TAG, "Deleted " + deleted + " expired account data report.");
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeGlideCodecs() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
import org.signal.ringrtc.CallLinkRootKey;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
@@ -108,14 +109,14 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onInviteToSignalClicked();
|
||||
void onActivatePaymentsClicked();
|
||||
void onSendPaymentClicked(@NonNull RecipientId recipientId);
|
||||
void onScheduledIndicatorClicked(@NonNull View view, @NonNull MessageRecord messageRecord);
|
||||
|
||||
void onScheduledIndicatorClicked(@NonNull View view, @NonNull ConversationMessage conversationMessage);
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
|
||||
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
|
||||
|
||||
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
|
||||
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
|
||||
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UsernameUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -19,7 +17,7 @@ import androidx.core.view.ViewCompat;
|
||||
import org.signal.qr.QrScannerView;
|
||||
import org.signal.qr.kitkat.ScanListener;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist;
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
|
||||
@@ -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.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
@@ -106,7 +107,9 @@ public class DeviceListFragment extends ListFragment
|
||||
if (data.isEmpty()) {
|
||||
empty.setVisibility(View.VISIBLE);
|
||||
TextSecurePreferences.setMultiDevice(getActivity(), false);
|
||||
SignalStore.misc().setHasLinkedDevices(false);
|
||||
} else {
|
||||
SignalStore.misc().setHasLinkedDevices(true);
|
||||
empty.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import android.view.Window;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
@@ -20,7 +22,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(this)
|
||||
.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) -> {
|
||||
|
||||