mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 08:12:46 +02:00
Compare commits
938 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3bfe604dbb | ||
|
ca61751a8b | ||
|
cce1c2252d | ||
|
6a4d887941 | ||
|
4de1759321 | ||
|
139cd051ab | ||
|
33b2abc3b9 | ||
|
c63128cfbd | ||
|
5063188b25 | ||
|
d09c03bff3 | ||
|
f444595845 | ||
|
eedf545409 | ||
|
aba8a3fed1 | ||
|
68d475dc55 | ||
|
ece9fbd3bb | ||
|
602d67155f | ||
|
75ef491e3e | ||
|
0f327fc75f | ||
|
a456f4c6a5 | ||
|
e097bd8117 | ||
|
ded8acc836 | ||
|
d8a0a1d38e | ||
|
e8f28d7ce4 | ||
|
a58bb776f3 | ||
|
4ba1a34f38 | ||
|
3d68b15e60 | ||
|
c78bba803c | ||
|
863c09142f | ||
|
3142442e5c | ||
|
4c1d50d554 | ||
|
25e7bbcd79 | ||
|
4b3a6a883d | ||
|
6c0bb2a949 | ||
|
f714566200 | ||
|
327a596de5 | ||
|
cc4603b61f | ||
|
f51568b331 | ||
|
6ceac578a3 | ||
|
1c733e6661 | ||
|
3842ec6bb0 | ||
|
ab1d652f17 | ||
|
70e90d8542 | ||
|
39e185576c | ||
|
9c402d4d40 | ||
|
37378ca5a6 | ||
|
a35749964c | ||
|
246f6bb0d0 | ||
|
a08a1d4f74 | ||
|
7acbd42a45 | ||
|
c6a5d05ffb | ||
|
36b17e9b8c | ||
|
c2cccd8b11 | ||
|
e7804af2f7 | ||
|
0412fabbd2 | ||
|
22959cddb2 | ||
|
7193db8344 | ||
|
352662d19a | ||
|
1afabb69c1 | ||
|
6f4ea83fa9 | ||
|
1c17bd9f5a | ||
|
de5f182f29 | ||
|
aa1843774a | ||
|
9e1c30ec5d | ||
|
31984a57d6 | ||
|
2f0645a94e | ||
|
c57d41863f | ||
|
25bbe9c3d6 | ||
|
c5c3592a4c | ||
|
5a8008a4dc | ||
|
2c5d2ea179 | ||
|
4387fd3327 | ||
|
1f2d5b0d00 | ||
|
253582219c | ||
|
3fc9fe3017 | ||
|
e07a584d66 | ||
|
150d44aafd | ||
|
179474b975 | ||
|
825e21362b | ||
|
4741169cc7 | ||
|
a8ad57a9b0 | ||
|
8582ad6015 | ||
|
51898a8109 | ||
|
d63f00851a | ||
|
f179fc523d | ||
|
eda29e3fef | ||
|
633548f190 | ||
|
811cbb2e20 | ||
|
6569ee5d10 | ||
|
5f60d7fd3b | ||
|
10f8aebde2 | ||
|
ea771476cc | ||
|
08bc487f17 | ||
|
1b6b71ed98 | ||
|
9f2631110e | ||
|
e8b1e418fa | ||
|
44563e73e2 | ||
|
6c0f775c4b | ||
|
ea3e467dc4 | ||
|
5a65eddf59 | ||
|
e979bee920 | ||
|
eff08955f1 | ||
|
28869f4382 | ||
|
8814364497 | ||
|
9c595b6c02 | ||
|
d49d0295a2 | ||
|
da7c971927 | ||
|
548879bd9f | ||
|
0c2516ccf8 | ||
|
332f227bc1 | ||
|
a98b2ecce3 | ||
|
195e2703b9 | ||
|
e38cb7c1a6 | ||
|
868d9cf55c | ||
|
aa3e68f3fd | ||
|
bf2d937ad6 | ||
|
e24d5b3ca4 | ||
|
e9778d6feb | ||
|
8c4c909f44 | ||
|
a1db8653ab | ||
|
cc5df1e1d5 | ||
|
87b1394e98 | ||
|
e3c2af2c59 | ||
|
a618a9214e | ||
|
76133ab55e | ||
|
2d4a728af4 | ||
|
4a2a6d34ae | ||
|
75c2dfcd48 | ||
|
6ebedaf540 | ||
|
85e8e652f1 | ||
|
3aabb17ea5 | ||
|
f1e5129acb | ||
|
e8dbed1642 | ||
|
c5ba746904 | ||
|
0855806ae2 | ||
|
a2c75e7c71 | ||
|
6f996f1f09 | ||
|
bcfd322b85 | ||
|
9dc831d8e5 | ||
|
b2f6476f78 | ||
|
b7d86c3fa4 | ||
|
89506b9e81 | ||
|
9e60f73bcf | ||
|
1e6d98a121 | ||
|
98d56cb556 | ||
|
8b1a07b8a8 | ||
|
92e809fa6d | ||
|
a0998e4aff | ||
|
804d712848 | ||
|
08cda2ee10 | ||
|
bf03b367f1 | ||
|
c1da4aecd7 | ||
|
38c54e0f2c | ||
|
9ebf87df62 | ||
|
32d2cea7f8 | ||
|
f998cb6b18 | ||
|
9d4e903c4a | ||
|
cfdf5cb552 | ||
|
e859357c6a | ||
|
e7f13c9efe | ||
|
2a68c8d08b | ||
|
04f0146afd | ||
|
92ecfafa0d | ||
|
f74ab2dfd4 | ||
|
a1dc383148 | ||
|
b853397c0a | ||
|
0cfd33fc8b | ||
|
51e63c5d1d | ||
|
e9ba7342d5 | ||
|
7434aed43f | ||
|
283f32479d | ||
|
c4ad90696f | ||
|
85ee183e4f | ||
|
586b0fe6e2 | ||
|
bdfce35d9a | ||
|
3648d6292a | ||
|
b8f88d323d | ||
|
b8f66a36d5 | ||
|
d4050a7b9d | ||
|
bfb8b6203c | ||
|
15223ecfe4 | ||
|
e098b87d0a | ||
|
e878821df2 | ||
|
596fcf94ba | ||
|
5008bfd6a9 | ||
|
2ef82f1b82 | ||
|
2f6b38eb39 | ||
|
69eaf2695e | ||
|
542d11d7f1 | ||
|
1de819b0a3 | ||
|
5713fa4f59 | ||
|
93fb40f323 | ||
|
f4314ebdae | ||
|
c62a7c4051 | ||
|
3e97e03ccc | ||
|
01e42838ef | ||
|
192c6db03e | ||
|
a98bd0940f | ||
|
c0623726ae | ||
|
ba8ec97f6a | ||
|
fb1c01c37c | ||
|
f8e35da533 | ||
|
53053d8f4a | ||
|
9eab1acf1e | ||
|
310517ece4 | ||
|
8adb36c7c4 | ||
|
057f6fdf26 | ||
|
b8b79de91c | ||
|
f86fa6cb5d | ||
|
693c980414 | ||
|
faeeec0e37 | ||
|
3db26bcae1 | ||
|
f0dbb92d76 | ||
|
73ce38c6a9 | ||
|
901cf15a79 | ||
|
4e3a948513 | ||
|
1ed8ff8711 | ||
|
4b74c7320b | ||
|
7ed9f535e6 | ||
|
f5c1ad8f2a | ||
|
174084a256 | ||
|
1e718bb44f | ||
|
f0fe10a11d | ||
|
ae1da6b9f5 | ||
|
04b6b3674d | ||
|
4254db5cc0 | ||
|
86e5a45621 | ||
|
397d4f0be5 | ||
|
01fdc6d1bb | ||
|
33698abfb2 | ||
|
80e8cd4191 | ||
|
2932e05851 | ||
|
19f16c9e56 | ||
|
cec5cd864c | ||
|
ba26aee54c | ||
|
6721e33c7e | ||
|
c14b226c92 | ||
|
4bb804fbf7 | ||
|
6cb82421e4 | ||
|
6e66c31911 | ||
|
a1ce245e3a | ||
|
668967546c | ||
|
5bd448405b | ||
|
fe235e0791 | ||
|
a9191b8fad | ||
|
63499c2f48 | ||
|
3fa2647e92 | ||
|
cdb1b8d8f8 | ||
|
c7c35399e5 | ||
|
102b8f88d0 | ||
|
4ca0c23e2a | ||
|
36de891451 | ||
|
f9d931960b | ||
|
dbe78f160b | ||
|
a6f4cd74d5 | ||
|
87a087c0b5 | ||
|
962e11a740 | ||
|
e658ef184d | ||
|
da472ea858 | ||
|
ff0b92272a | ||
|
cca6d0e967 | ||
|
e0ea0c195b | ||
|
b3d4d20195 | ||
|
5f788d962e | ||
|
2650453d4b | ||
|
4d6ba5a491 | ||
|
32721caf5a | ||
|
ad084e1fec | ||
|
512e4f0ce3 | ||
|
56f8e52352 | ||
|
a93cbf3548 | ||
|
e0e4cf3df1 | ||
|
16bd642ae8 | ||
|
a03f69fb98 | ||
|
8787f5d920 | ||
|
ef1ae4105b | ||
|
7f2ce91c82 | ||
|
1f30cf468a | ||
|
762dd1d0a5 | ||
|
f5790e5dc2 | ||
|
e1a12f4c77 | ||
|
52eec06110 | ||
|
a6e4a328b3 | ||
|
bfebaa5c6c | ||
|
05e848244e | ||
|
c23819bfcf | ||
|
b0034f91b0 | ||
|
caa7709090 | ||
|
51b7a0aeae | ||
|
51228a3a5c | ||
|
5c091339f0 | ||
|
6643923981 | ||
|
4ce2478e44 | ||
|
6131c10d31 | ||
|
8c5ec2c57f | ||
|
e046d17bf2 | ||
|
8e7166662b | ||
|
3aaa425714 | ||
|
a66010a1d8 | ||
|
e9706a3b64 | ||
|
a6e61f4de8 | ||
|
e758ede706 | ||
|
957fe189dc | ||
|
12a4f6f05b | ||
|
bcd78a96bf | ||
|
f3b464b88a | ||
|
369f40c804 | ||
|
bddd7f4005 | ||
|
5434c39453 | ||
|
e4ac28877c | ||
|
8df7797f6d | ||
|
aa2b62976e | ||
|
e3dc6e307f | ||
|
48a30a7b82 | ||
|
1365240f69 | ||
|
1cd27d7f67 | ||
|
84c8f9d351 | ||
|
7e703e4788 | ||
|
fb13b402af | ||
|
e599abc6ba | ||
|
5784c4c8b3 | ||
|
cec79fed44 | ||
|
31a7563de5 | ||
|
0ad2058a1a | ||
|
44365d70fe | ||
|
bbf0b1e3ba | ||
|
8972319a85 | ||
|
85f713c8c7 | ||
|
6e020cada2 | ||
|
d436a7bad7 | ||
|
16a275529d | ||
|
0ad493d860 | ||
|
30774957ba | ||
|
8a1a772ab7 | ||
|
0d9a10f5fa | ||
|
1c9907c5d2 | ||
|
050530a991 | ||
|
f984cff5b4 | ||
|
ad7297c7e3 | ||
|
17d90a32e1 | ||
|
89a20eafd8 | ||
|
a2be821d2f | ||
|
39022b3b66 | ||
|
fe8f79698c | ||
|
a0a8f95d37 | ||
|
7247b4471a | ||
|
6bce62a598 | ||
|
629c73e630 | ||
|
fb0d262336 | ||
|
28df09a8d4 | ||
|
2e93b7c4c9 | ||
|
4125baf066 | ||
|
1fb2569a39 | ||
|
19ed5a2d3d | ||
|
84272f5b5d | ||
|
90804f7625 | ||
|
824dafb525 | ||
|
3fcf77e214 | ||
|
c5d2a34ebd | ||
|
a04a0e7175 | ||
|
9aa0d38116 | ||
|
f8452429a4 | ||
|
4349331ee7 | ||
|
1d3dbdf989 | ||
|
60c873aa66 | ||
|
674fa0e5ce | ||
|
f28e3ca504 | ||
|
96d6a72b97 | ||
|
7b3fa501c6 | ||
|
16dbcc43b0 | ||
|
c0c300925d | ||
|
0a02a08ed4 | ||
|
95c05f6107 | ||
|
e24785015d | ||
|
554c37febe | ||
|
dbb9dc4458 | ||
|
c6dba21212 | ||
|
94b5e4e2f8 | ||
|
0d891b1c93 | ||
|
2f77deb0a8 | ||
|
fa0adb17b8 | ||
|
36bd2290e9 | ||
|
372a5ba1ec | ||
|
25f45da195 | ||
|
9d191a783d | ||
|
0dc3593660 | ||
|
f609cc7042 | ||
|
360666a758 | ||
|
3f44056243 | ||
|
d075cbf69b | ||
|
6b13c00d56 | ||
|
6e95c6c200 | ||
|
35fed2676a | ||
|
9754e26e5f | ||
|
af9295723c | ||
|
55993aff04 | ||
|
66c5a35f36 | ||
|
c1260dcb9b | ||
|
f4e7405d92 | ||
|
c15cc34bfd | ||
|
16f32da647 | ||
|
4c34d73501 | ||
|
df1bd62f47 | ||
|
3ced179fbb | ||
|
957ceff87c | ||
|
85fcc2eab7 | ||
|
e7143b53d5 | ||
|
b728e10616 | ||
|
8ffa0061e9 | ||
|
b767c2fa54 | ||
|
aeb41bc516 | ||
|
178bdff62a | ||
|
3e79da6a79 | ||
|
f762c4c7a2 | ||
|
8de0bdca93 | ||
|
ef2fcd60d7 | ||
|
55b61775e8 | ||
|
07e57b1498 | ||
|
b522c9f62f | ||
|
04a7c57d64 | ||
|
367da0c78f | ||
|
057c21f7d0 | ||
|
af8ab57e60 | ||
|
409b6b807e | ||
|
4a454f0817 | ||
|
6b806922ee | ||
|
64a67b57b8 | ||
|
76bcf9dcf7 | ||
|
234dfa18d3 | ||
|
ba0823f4d0 | ||
|
9d401512d3 | ||
|
464e99505b | ||
|
17cf3fd7ad | ||
|
60998c9146 | ||
|
bd19225219 | ||
|
2868d62185 | ||
|
2a3d20d300 | ||
|
30dee07a3b | ||
|
96ecb1d07e | ||
|
99056a7807 | ||
|
374790176f | ||
|
f3e2a55869 | ||
|
666f3ea152 | ||
|
25fe56116c | ||
|
b27eead016 | ||
|
77a01f0cd4 | ||
|
5dfa08ace6 | ||
|
4c61dfef62 | ||
|
9653f082a3 | ||
|
3e2d892fb5 | ||
|
30d47b4fa6 | ||
|
fd3f591541 | ||
|
c85ba51274 | ||
|
843da1d48d | ||
|
d8cf44fdc9 | ||
|
8662797cf8 | ||
|
eabb0bb41d | ||
|
4966bef9c3 | ||
|
0f625c27a1 | ||
|
248b9ff1e1 | ||
|
39f3a1c697 | ||
|
c6100fc26c | ||
|
84b474d070 | ||
|
c4b977c6e1 | ||
|
a1907aaddb | ||
|
56ed56a986 | ||
|
91f28bfb8a | ||
|
46d7db8214 | ||
|
b5cdb44642 | ||
|
cb964c6dcd | ||
|
e79a4771c1 | ||
|
9006acb66a | ||
|
435a6b2f1a | ||
|
4d288ddd55 | ||
|
24cea5110e | ||
|
79f804b2d4 | ||
|
4b85e39e3e | ||
|
8f5918de4d | ||
|
ae762aa928 | ||
|
928da82dde | ||
|
94ea857738 | ||
|
0bb92e9e91 | ||
|
df4aab1d73 | ||
|
d3f93984d4 | ||
|
125d61eb68 | ||
|
c0988ba6d9 | ||
|
03b9904b07 | ||
|
24a9931abd | ||
|
2581a3433e | ||
|
8c9ca1e0f2 | ||
|
dcae051e85 | ||
|
3d03bf6f91 | ||
|
54b154f85f | ||
|
37c926d178 | ||
|
f50f81d321 | ||
|
743ace7e60 | ||
|
5d476e7259 | ||
|
fb6bcc8470 | ||
|
bda1633979 | ||
|
4169f580b8 | ||
|
4a4edcf82a | ||
|
a1fc0db8a2 | ||
|
dc19652c2b | ||
|
d2f1488934 | ||
|
bbdf6c6eaf | ||
|
f532d28fb3 | ||
|
0135368328 | ||
|
af81a52746 | ||
|
86b4c9ac73 | ||
|
1df3b4e18f | ||
|
fa204eca3b | ||
|
2c34fe2dc3 | ||
|
9c34187391 | ||
|
10d78a3102 | ||
|
46378845e9 | ||
|
ff39b22686 | ||
|
601e11980f | ||
|
8cef299878 | ||
|
914bfb105c | ||
|
076ecca5a9 | ||
|
840648b750 | ||
|
29fd4c4bd2 | ||
|
905fa7dd86 | ||
|
e8af71f641 | ||
|
6901d368d2 | ||
|
e89a340fae | ||
|
c3d6135fb3 | ||
|
6611defd87 | ||
|
5c449a9e30 | ||
|
415ac6a748 | ||
|
7ddafa098f | ||
|
177f7b9cb3 | ||
|
fb1704b717 | ||
|
ade1f1b911 | ||
|
344a9836d2 | ||
|
2c1487d303 | ||
|
e1e505e695 | ||
|
c5cd68416f | ||
|
45f757e157 | ||
|
de77c8a210 | ||
|
54b3af2c88 | ||
|
171a945de9 | ||
|
d845d10d73 | ||
|
98d91a746e | ||
|
1be1b5b263 | ||
|
dafb0c8d5c | ||
|
6ca69a9947 | ||
|
ff171a39c6 | ||
|
8acee57118 | ||
|
d03ab989e6 | ||
|
4f5b1d9646 | ||
|
f8dca1237a | ||
|
c382f706a6 | ||
|
23cbed310a | ||
|
1786ba30f7 | ||
|
530ce0952c | ||
|
262f10af9a | ||
|
3430a8f3ea | ||
|
959be3a23d | ||
|
15306fc116 | ||
|
fb5013db88 | ||
|
ab4780c9b9 | ||
|
e6a71ab7de | ||
|
6aecf68098 | ||
|
dc54c6139f | ||
|
71f158c526 | ||
|
dc7067baff | ||
|
ced226777f | ||
|
579d4f7a5b | ||
|
60b91d4d50 | ||
|
917042c48c | ||
|
ed27c35bb4 | ||
|
c35c94b7b7 | ||
|
d8317f7439 | ||
|
9a3813f883 | ||
|
0a7f77ea16 | ||
|
d6329a1ab6 | ||
|
0ba120356a | ||
|
d402b49f07 | ||
|
d4ba9fa09a | ||
|
62f352243d | ||
|
1ad19b5e93 | ||
|
e7ba0a5c60 | ||
|
050407f7c7 | ||
|
dffe096c59 | ||
|
bf5ad2cf18 | ||
|
a8ae4ddde8 | ||
|
2596a9ef7e | ||
|
f74989ac1d | ||
|
d3bc4017a5 | ||
|
ab8480d983 | ||
|
50dcc24900 | ||
|
a66470f93b | ||
|
d2dba1826d | ||
|
7f02195377 | ||
|
0002cddd67 | ||
|
19e1683106 | ||
|
5c538c7865 | ||
|
ee23967afe | ||
|
86dd8f3fd8 | ||
|
f19e2a0995 | ||
|
afd1002fdb | ||
|
07c7ed0a4e | ||
|
b48eb6e9cc | ||
|
e5da5a34cb | ||
|
f3a2c467ed | ||
|
a1fd35aa67 | ||
|
ad8ed37ff6 | ||
|
088e8bc9f9 | ||
|
3714323d74 | ||
|
d70b19fa93 | ||
|
08693a6875 | ||
|
73eca2407b | ||
|
ae7a52cecf | ||
|
2e244dd448 | ||
|
1ad77530aa | ||
|
e44963728f | ||
|
9075371145 | ||
|
3b256a708e | ||
|
aea517515c | ||
|
f28889284d | ||
|
240f1f51b9 | ||
|
05efd7423e | ||
|
fce8a3bc18 | ||
|
2adb5af051 | ||
|
8b7e5e527a | ||
|
c3d8916802 | ||
|
532f5e58ea | ||
|
57a5714fb5 | ||
|
a3fd49499b | ||
|
3dc2cd4d7a | ||
|
0b738e3d9e | ||
|
b29c2b2de4 | ||
|
40f2d19f81 | ||
|
2e997f2c67 | ||
|
0fd50892af | ||
|
55bd346cb2 | ||
|
0724ac133b | ||
|
0507fa5b0e | ||
|
617e945afa | ||
|
870c4bf765 | ||
|
9a592e9c7e | ||
|
53592ac404 | ||
|
d8848a6062 | ||
|
173c1d3a6e | ||
|
c815c4080d | ||
|
c09626182c | ||
|
65c6ce3033 | ||
|
06cc2f527e | ||
|
9da5061689 | ||
|
82b4415f7d | ||
|
3f1e5b9b1e | ||
|
1b95d98ccd | ||
|
e2e1925796 | ||
|
52c8f24df5 | ||
|
ca855da8ae | ||
|
a39d35e54c | ||
|
3bd2b24b10 | ||
|
12448426d4 | ||
|
a043d7cac7 | ||
|
4d88111d48 | ||
|
34d6610bf8 | ||
|
4bfc52327b | ||
|
15d5f7ff55 | ||
|
91ecd1aeeb | ||
|
342f871916 | ||
|
18d51de0b9 | ||
|
4dc6cae854 | ||
|
5fb160e1ac | ||
|
f1589314c2 | ||
|
7fb9a550c8 | ||
|
46f7f4814c | ||
|
c002cc104c | ||
|
492128a621 | ||
|
fff0f09684 | ||
|
64054d13a3 | ||
|
ae8d7096c3 | ||
|
13e04b6362 | ||
|
89e557bcef | ||
|
fdf4749c1b | ||
|
a63608bf97 | ||
|
5c101e9466 | ||
|
4b25ebc731 | ||
|
8e88fcb462 | ||
|
fd19b345a1 | ||
|
4a7bba047b | ||
|
440c21e9f3 | ||
|
959b679086 | ||
|
64cfd4d81a | ||
|
c742ca3b41 | ||
|
2002252f72 | ||
|
7df8b3a9bf | ||
|
3fea2173f4 | ||
|
d125cb5c01 | ||
|
86c9264ed9 | ||
|
00f2d0249f | ||
|
4465e6eea3 | ||
|
b87fb8c396 | ||
|
d4706b38b8 | ||
|
31c82b4ba6 | ||
|
cb80d8d349 | ||
|
7be3434136 | ||
|
ec1422b0f0 | ||
|
b0a6eaaa96 | ||
|
d1c4d4b099 | ||
|
d35905787d | ||
|
9a972b2f73 | ||
|
1fe0c8a3e9 | ||
|
116bab5bc8 | ||
|
c76eb3bc98 | ||
|
81c1717384 | ||
|
0fd0500d30 | ||
|
34d638da4f | ||
|
e39b177b5b | ||
|
07aa3ee64c | ||
|
c94856cdf8 | ||
|
4dd0c04537 | ||
|
99c409b6d2 | ||
|
8cb2c2532f | ||
|
6f804cab4d | ||
|
cf3dbb378e | ||
|
e32716aa48 | ||
|
ba46f10e3f | ||
|
4b37ede8c2 | ||
|
dbe4c0c8e4 | ||
|
4f4afd6840 | ||
|
7409fde650 | ||
|
cfa31e6332 | ||
|
df973a6275 | ||
|
0eb36a607b | ||
|
0509e76f18 | ||
|
e379ccf086 | ||
|
7ae52d676d | ||
|
3d33018ffa | ||
|
860595520b | ||
|
ae318a835e | ||
|
7a3dbecc08 | ||
|
c52aae7c29 | ||
|
f0f3e8ddb9 | ||
|
a95102a78f | ||
|
2adafbeb03 | ||
|
f3a5fb7fe3 | ||
|
907a786b1a | ||
|
e3ed3e5b05 | ||
|
a2b366ebfe | ||
|
f7de2f0f13 | ||
|
919225bdfd | ||
|
88cba74cac | ||
|
e9ca876444 | ||
|
0992e76800 | ||
|
8a9498bae4 | ||
|
d2598480c8 | ||
|
6e57b06673 | ||
|
a036be6436 | ||
|
53c7ea2831 | ||
|
e117fec74f | ||
|
6e01b75b2f | ||
|
691e7fe616 | ||
|
e793a46576 | ||
|
fa48e8cffa | ||
|
69cf437a39 | ||
|
ebbc432570 | ||
|
2532724e92 | ||
|
e31693b4b7 | ||
|
3ed6452232 | ||
|
166aaa62f0 | ||
|
45e5fff622 | ||
|
8f2dba09ee | ||
|
2ea46c5e54 | ||
|
fc5f0f7673 | ||
|
188f4a2e72 | ||
|
cf97fc3b01 | ||
|
5267ba240a | ||
|
f185dcacd7 | ||
|
330a33a0e8 | ||
|
b75b299847 | ||
|
7c59bcc928 | ||
|
0e110b0794 | ||
|
e156a62e19 | ||
|
628439aa65 | ||
|
d49fcb80fc | ||
|
00fd067c6b | ||
|
ca37895619 | ||
|
43497e0da9 | ||
|
efa510c0a8 | ||
|
0828159ee4 | ||
|
90f21198c3 | ||
|
a0b0778fce | ||
|
d937fa67ad | ||
|
91d396fbca | ||
|
afba5b2b6c | ||
|
b579a1bc83 | ||
|
db49673fc7 | ||
|
bb0511e659 | ||
|
0e0763ec2d | ||
|
3faf42be53 | ||
|
dea903bcb5 | ||
|
e1c6542e03 | ||
|
01484978bd | ||
|
cad14c93d0 | ||
|
ee52b9185b | ||
|
861a7f791f | ||
|
f2fa57224b | ||
|
e0977dd97b | ||
|
f47bef71a4 | ||
|
92985fc8e7 | ||
|
243b0a7d82 | ||
|
92c719a803 | ||
|
f4108ae0eb | ||
|
ecf3fee709 | ||
|
e67e472025 | ||
|
a6541481bf | ||
|
85a4f83662 | ||
|
22955e6b34 | ||
|
9520aff848 | ||
|
6b09a78ece | ||
|
789bcc8d77 | ||
|
2914117a8e | ||
|
8049962a99 | ||
|
225b1c380e | ||
|
60d80ea0ba | ||
|
c8211098f3 | ||
|
e78fde4eca | ||
|
59d60813fb | ||
|
4c31e52892 | ||
|
c646fd2b36 | ||
|
6432859732 | ||
|
2beef7d816 | ||
|
623056455b | ||
|
7a4d9370e3 | ||
|
fe3138492e | ||
|
05a52164f3 | ||
|
d14f1dd1ab | ||
|
88e8c11ee5 | ||
|
7afc7bdb31 | ||
|
84a3754c9f | ||
|
4b2f8e9174 | ||
|
a17932e17e | ||
|
084c27a2bb | ||
|
ed2f62cbe7 | ||
|
38fb7185b6 | ||
|
ce42d2fb8a | ||
|
af3fc22e2d | ||
|
b659cb60a2 | ||
|
34cf9903dc | ||
|
062a21e39a | ||
|
4510aff00a | ||
|
d0953b8406 | ||
|
7822660ce7 | ||
|
bdfcf5c67c | ||
|
ae0d09a049 | ||
|
69759b7415 | ||
|
7e8e1ab9b7 | ||
|
b44f5d3b4a | ||
|
03f8b66993 | ||
|
e411f139c8 | ||
|
e962d1dadf | ||
|
756b0febe6 | ||
|
1535f3e2e5 | ||
|
3e808dec90 | ||
|
637f4a8350 | ||
|
d3bc9f52fd | ||
|
ffd8ac859d | ||
|
6e43e9b51c | ||
|
426171508e | ||
|
e86460b578 | ||
|
8dd5f88dba | ||
|
3aa6de7cf5 | ||
|
a75242c79d | ||
|
784918350b | ||
|
0199cf9a03 | ||
|
ab6e7a3b8a | ||
|
f489265ce7 | ||
|
6c9c3e5cb3 | ||
|
9b7c2599a7 | ||
|
25bbd7c526 | ||
|
e0c3f3638d | ||
|
d45653dbb3 | ||
|
f70623beea | ||
|
e542e4ba22 | ||
|
05d1e64cb5 | ||
|
adac80062c | ||
|
28f8d9500e | ||
|
538bda329e | ||
|
a00ddca188 | ||
|
8ac2cb0530 | ||
|
19d655ec41 | ||
|
4961bb0e08 | ||
|
f45040c405 | ||
|
9fed62e4a7 | ||
|
43b9e2cec9 | ||
|
3185b88fe5 | ||
|
c2906d9b06 | ||
|
cfb615f972 | ||
|
9c22c0952c | ||
|
5a834619c0 | ||
|
3b62f50f7b | ||
|
18e804d174 | ||
|
c105d82027 | ||
|
860921217d | ||
|
3fe2f2876a | ||
|
e84fd408be | ||
|
f361fd7355 | ||
|
458e3ee5e8 | ||
|
5fa247a0c5 | ||
|
48e58967b2 | ||
|
0a41a9f773 | ||
|
22e3b370e3 | ||
|
8f5589d3e1 | ||
|
03389fc040 | ||
|
996aa9ef66 | ||
|
9778999a7f | ||
|
91301197ea | ||
|
01d6b52a60 | ||
|
ce884ac577 | ||
|
b047f36e86 | ||
|
4fdd2f4eed | ||
|
182753e4ec | ||
|
29c9d3070c | ||
|
ffeae7ec83 | ||
|
db77e7b817 | ||
|
fcee85a682 | ||
|
17ddb5ce43 | ||
|
53583c691f | ||
|
2b9d3960b3 | ||
|
92befcde5d | ||
|
697eaec197 | ||
|
86fba28313 | ||
|
f3c3c07d46 | ||
|
8966e24925 | ||
|
becc5a7b54 | ||
|
a61434ae08 | ||
|
20b726819f | ||
|
bfd847179f | ||
|
7e955ef0e4 | ||
|
2697800deb | ||
|
2c47fe9f0d | ||
|
aa16ba88ae | ||
|
a2367ef14f |
4
.idea/dictionaries/bmarty.xml
generated
4
.idea/dictionaries/bmarty.xml
generated
@@ -7,17 +7,21 @@
|
||||
<w>ciphertext</w>
|
||||
<w>coroutine</w>
|
||||
<w>decryptor</w>
|
||||
<w>displayname</w>
|
||||
<w>emoji</w>
|
||||
<w>emojis</w>
|
||||
<w>fdroid</w>
|
||||
<w>gplay</w>
|
||||
<w>hmac</w>
|
||||
<w>homeserver</w>
|
||||
<w>ktlint</w>
|
||||
<w>linkified</w>
|
||||
<w>linkify</w>
|
||||
<w>megolm</w>
|
||||
<w>msisdn</w>
|
||||
<w>msisdns</w>
|
||||
<w>pbkdf</w>
|
||||
<w>pids</w>
|
||||
<w>pkcs</w>
|
||||
<w>riotx</w>
|
||||
<w>signin</w>
|
||||
|
177
CHANGES.md
177
CHANGES.md
@@ -1,3 +1,180 @@
|
||||
Changes in RiotX 0.23.0 (2020-08-05)
|
||||
===================================================
|
||||
|
||||
This is the very last version of RiotX, published on the PlayStore.
|
||||
|
||||
This branch will never be merged on develop.
|
||||
|
||||
Features ✨:
|
||||
- Inform user that the app will not be updated anymore (#1727)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Video calls are shown as a voice ones in the timeline (#1676)
|
||||
- Fix regression: not able to create a room without IS configured (#1679)
|
||||
|
||||
Changes in Riot.imX 0.91.5 (2020-07-11)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- 3pid invite: it is now possible to invite people by email. An Identity Server has to be configured (#548)
|
||||
|
||||
Improvements 🙌:
|
||||
- Cleaning chunks with lots of events as long as a threshold has been exceeded (35_000 events in DB) (#1634)
|
||||
- Creating and listening to EventInsertEntity. (#1634)
|
||||
- Handling (almost) properly the groups fetching (#1634)
|
||||
- Improve fullscreen media display (#327)
|
||||
- Setup server recovery banner (#1648)
|
||||
- Set up SSSS from security settings (#1567)
|
||||
- New lab setting to add 'unread notifications' tab to main screen
|
||||
- Render third party invite event (#548)
|
||||
- Display three pid invites in the room members list (#548)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
|
||||
- Regression Composer does not grow, crops out text (#1650)
|
||||
- Bug / Unwanted draft (#698)
|
||||
- All users seems to be able to see the enable encryption option in room settings (#1341)
|
||||
- Leave room only leaves the current version (#1656)
|
||||
- Regression | Share action menu do not work (#1647)
|
||||
- verification issues on transition (#1555)
|
||||
- Fix issue when restoring keys backup using recovery key
|
||||
|
||||
SDK API changes ⚠️:
|
||||
- CreateRoomParams has been updated
|
||||
|
||||
Build 🧱:
|
||||
- Upgrade some dependencies
|
||||
- Revert to build-tools 3.5.3
|
||||
|
||||
Other changes:
|
||||
- Use Intent.ACTION_CREATE_DOCUMENT to save megolm key or recovery key in a txt file
|
||||
- Use `Context#withStyledAttributes` extension function (#1546)
|
||||
|
||||
Changes in Riot.imX 0.91.4 (2020-07-06)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Re-activate Wellknown support with updated UI (#1614)
|
||||
|
||||
Improvements 🙌:
|
||||
- Upload device keys only once to the homeserver and fix crash when no network (#1629)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix crash when coming from a notification (#1601)
|
||||
- Fix Exception when importing keys (#1576)
|
||||
- File isn't downloaded when another file with the same name already exists (#1578)
|
||||
- saved images don't show up in gallery (#1324)
|
||||
- Fix reply fallback leaking sender locale (#429)
|
||||
|
||||
Build 🧱:
|
||||
- Fix lint false-positive about WorkManager (#1012)
|
||||
- Upgrade build-tools from 3.5.3 to 3.6.3
|
||||
- Upgrade gradle from 5.4.1 to 5.6.4
|
||||
|
||||
Changes in Riot.imX 0.91.3 (2020-07-01)
|
||||
===================================================
|
||||
|
||||
Notes:
|
||||
- This version is the third beta version of RiotX codebase published as Riot-Android on the PlayStore.
|
||||
- Changelog below includes changes of v0.91.0, v0.91.1, and v0.91.2, because the first beta versions have been tagged and
|
||||
published from the branch feature/migration_from_legacy.
|
||||
- This version uses temporary name `Riot.imX`, to distinguish the app with RiotX app.
|
||||
|
||||
Features ✨:
|
||||
- Call with WebRTC support (##611)
|
||||
- Add capability to change the display name (#1529)
|
||||
|
||||
Improvements 🙌:
|
||||
- "Add Matrix app" menu is now always visible (#1495)
|
||||
- Handle `/op`, `/deop`, and `/nick` commands (#12)
|
||||
- Prioritising Recovery key over Recovery passphrase (#1463)
|
||||
- Room Settings: Name, Topic, Photo, Aliases, History Visibility (#1455)
|
||||
- Update user avatar (#1054)
|
||||
- Allow self-signed certificate (#1564)
|
||||
- Improve file download and open in timeline
|
||||
- Catchup tab is removed temporarily (#1565)
|
||||
- Render room avatar change (#1319)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix dark theme issue on login screen (#1097)
|
||||
- Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519)
|
||||
- User could not redact message that they have sent (#1543)
|
||||
- Use vendor prefix for non merged MSC (#1537)
|
||||
- Compress images before sending (#1333)
|
||||
- Searching by displayname is case sensitive (#1468)
|
||||
- Fix layout overlap issue (#1407)
|
||||
|
||||
Build 🧱:
|
||||
- Enable code optimization (Proguard)
|
||||
- SDK is now API level 21 minimum, and so RiotX (#405)
|
||||
|
||||
Other changes:
|
||||
- Use `SharedPreferences#edit` extension function consistently (#1545)
|
||||
- Use `retrofit2.Call.awaitResponse` extension provided by Retrofit 2. (#1526)
|
||||
- Fix minor typo in contribution guide (#1512)
|
||||
- Fix self-assignment of callback in `DefaultRoomPushRuleService#setRoomNotificationState` (#1520)
|
||||
- Random housekeeping clean-ups indicated by Lint (#1520, #1541)
|
||||
- Keys Backup API now use the unstable prefix (#1503)
|
||||
- Remove deviceId from /keys/upload/{deviceId} as not spec-compliant (#1502)
|
||||
|
||||
Changes in RiotX 0.22.0 (2020-06-15)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Integration Manager and Widget support (#48)
|
||||
- Send stickers (#51)
|
||||
|
||||
Improvements 🙌:
|
||||
- New wording for notice when current user is the sender
|
||||
- Hide "X made no changes" event by default in timeline (#1430)
|
||||
- Hide left rooms in breadcrumbs (#766)
|
||||
- Handle PowerLevel properly (#627)
|
||||
- Correctly handle SSO login redirection
|
||||
- SSO login is now performed in the default browser, or in Chrome Custom tab if available (#1400)
|
||||
- Improve checking of homeserver version support (#1442)
|
||||
- Add capability to add and remove a room from the favorites (#1217)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Switch theme is not fully taken into account without restarting the app
|
||||
- Temporary fix to show error when user is creating an account on matrix.org with userId containing only digits (#1410)
|
||||
- Reply composer overlay stays on screen too long after send (#1169)
|
||||
- Fix navigation bar icon contrast on API in [21,27[ (#1342)
|
||||
- Fix status bar icon contrast on API in [21,23[
|
||||
- Wrong /query request (#1444)
|
||||
- Make Credentials.homeServer optional because it is deprecated (#1443)
|
||||
- Fix issue on dark themes, after alert popup dismiss
|
||||
|
||||
Other changes:
|
||||
- Send plain text in the body of events containing formatted body, as per https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
|
||||
- Update link to Modular url from "https://modular.im/" to "https://modular.im/services/matrix-hosting-riot" and open it using ChromeCustomTab
|
||||
|
||||
Changes in RiotX 0.21.0 (2020-05-28)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Identity server support (#607)
|
||||
- Switch language support (#41)
|
||||
- Display list of attachments of a room (#860)
|
||||
|
||||
Improvements 🙌:
|
||||
- Better connectivity lost indicator when airplane mode is on
|
||||
- Add a setting to hide redacted events (#951)
|
||||
- Render formatted_body for m.notice and m.emote (#1196)
|
||||
- Change icon to magnifying-glass to filter room (#1384)
|
||||
|
||||
Bugfix 🐛:
|
||||
- After jump to unread, newer messages are never loaded (#1008)
|
||||
- Fix issues with FontScale switch (#69, #645)
|
||||
- "Seen by" uses 12h time (#1378)
|
||||
- Enable markdown (if active) when sending emote (#734)
|
||||
- Screenshots for Rageshake now includes Dialogs such as BottomSheet (#1349)
|
||||
|
||||
SDK API changes ⚠️:
|
||||
- initialize with proxy configuration
|
||||
|
||||
Other changes:
|
||||
- support new key agreement method for SAS (#1374)
|
||||
|
||||
Changes in RiotX 0.20.0 (2020-05-15)
|
||||
===================================================
|
||||
|
||||
|
@@ -19,7 +19,11 @@ An Android Studio template has been added to the project to help creating all fi
|
||||
|
||||
To install the template (to be done only once):
|
||||
- Go to folder `./tools/template`.
|
||||
- Run the script `./configure.sh`.
|
||||
- Mac OSX: Run the script `./configure.sh`.
|
||||
|
||||
Linux: Run `ANDROID_STUDIO=/path/to/android-studio ./configure`
|
||||
- e.g. `ANDROID_STUDIO=/usr/local/android-studio ./configure`
|
||||
|
||||
- Restart Android Studio.
|
||||
|
||||
To create a new screen:
|
||||
@@ -27,7 +31,7 @@ To create a new screen:
|
||||
- Then right click on the package, and select `New/New Vector/RiotX Feature`.
|
||||
- Follow the Wizard, especially replace `Main` by something more relevant to your feature.
|
||||
- Click on `Finish`.
|
||||
- Remainning steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
|
||||
- Remaining steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
|
||||
|
||||
Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect.
|
||||
|
||||
|
@@ -11,8 +11,8 @@ RiotX is an Android Matrix Client currently in beta but in active development.
|
||||
|
||||
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented.
|
||||
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
|
||||
[<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.riotx)
|
||||
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
|
||||
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.riotx)
|
||||
|
||||
Nightly build: [](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
|
||||
|
1
attachment-viewer/.gitignore
vendored
Normal file
1
attachment-viewer/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
78
attachment-viewer/build.gradle
Normal file
78
attachment-viewer/build.gradle
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
content {
|
||||
// PhotoView
|
||||
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||
}
|
||||
}
|
||||
jcenter()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.3.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
}
|
0
attachment-viewer/consumer-rules.pro
Normal file
0
attachment-viewer/consumer-rules.pro
Normal file
21
attachment-viewer/proguard-rules.pro
vendored
Normal file
21
attachment-viewer/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
attachment-viewer/src/main/AndroidManifest.xml
Normal file
2
attachment-viewer/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="im.vector.riotx.attachmentviewer" />
|
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
|
||||
class AnimatedImageViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView) {
|
||||
|
||||
val touchImageView: ImageView = itemView.findViewById(R.id.imageView)
|
||||
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
|
||||
|
||||
internal val target = DefaultImageLoaderTarget(this, this.touchImageView)
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
sealed class AttachmentEvents {
|
||||
data class VideoEvent(val isPlaying: Boolean, val progress: Int, val duration: Int) : AttachmentEvents()
|
||||
}
|
||||
|
||||
interface AttachmentEventListener {
|
||||
fun onEvent(event: AttachmentEvents)
|
||||
}
|
||||
|
||||
sealed class AttachmentCommands {
|
||||
object PauseVideo : AttachmentCommands()
|
||||
object StartVideo : AttachmentCommands()
|
||||
data class SeekTo(val percentProgress: Int) : AttachmentCommands()
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
|
||||
sealed class AttachmentInfo(open val uid: String) {
|
||||
data class Image(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid)
|
||||
data class AnimatedImage(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid)
|
||||
data class Video(override val uid: String, val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo(uid)
|
||||
// data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid)
|
||||
// data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid)
|
||||
}
|
||||
|
||||
interface AttachmentSourceProvider {
|
||||
|
||||
fun getItemCount(): Int
|
||||
|
||||
fun getAttachmentInfoAt(position: Int): AttachmentInfo
|
||||
|
||||
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image)
|
||||
|
||||
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage)
|
||||
|
||||
fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video)
|
||||
|
||||
fun overlayViewAtPosition(context: Context, position: Int): View?
|
||||
|
||||
fun clear(id: String)
|
||||
}
|
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.transition.TransitionManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import kotlinx.android.synthetic.main.activity_attachment_viewer.*
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.abs
|
||||
|
||||
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
|
||||
|
||||
lateinit var pager2: ViewPager2
|
||||
lateinit var imageTransitionView: ImageView
|
||||
lateinit var transitionImageContainer: ViewGroup
|
||||
|
||||
var topInset = 0
|
||||
var bottomInset = 0
|
||||
var systemUiVisibility = true
|
||||
|
||||
private var overlayView: View? = null
|
||||
set(value) {
|
||||
if (value == overlayView) return
|
||||
overlayView?.let { rootContainer.removeView(it) }
|
||||
rootContainer.addView(value)
|
||||
value?.updatePadding(top = topInset, bottom = bottomInset)
|
||||
field = value
|
||||
}
|
||||
|
||||
private lateinit var swipeDismissHandler: SwipeToDismissHandler
|
||||
private lateinit var directionDetector: SwipeDirectionDetector
|
||||
private lateinit var scaleDetector: ScaleGestureDetector
|
||||
private lateinit var gestureDetector: GestureDetectorCompat
|
||||
|
||||
var currentPosition = 0
|
||||
|
||||
private var swipeDirection: SwipeDirection? = null
|
||||
|
||||
private fun isScaled() = attachmentsAdapter.isScaled(currentPosition)
|
||||
|
||||
private var wasScaled: Boolean = false
|
||||
private var isSwipeToDismissAllowed: Boolean = true
|
||||
private lateinit var attachmentsAdapter: AttachmentsAdapter
|
||||
private var isOverlayWasClicked = false
|
||||
|
||||
// private val shouldDismissToBottom: Boolean
|
||||
// get() = e == null
|
||||
// || !externalTransitionImageView.isRectVisible
|
||||
// || !isAtStartPosition
|
||||
|
||||
private var isImagePagerIdle = true
|
||||
|
||||
fun setSourceProvider(sourceProvider: AttachmentSourceProvider) {
|
||||
attachmentsAdapter.attachmentSourceProvider = sourceProvider
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// This is important for the dispatchTouchEvent, if not we must correct
|
||||
// the touch coordinates
|
||||
window.decorView.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE)
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
||||
|
||||
setContentView(R.layout.activity_attachment_viewer)
|
||||
attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
|
||||
attachmentsAdapter = AttachmentsAdapter()
|
||||
attachmentPager.adapter = attachmentsAdapter
|
||||
imageTransitionView = transitionImageView
|
||||
transitionImageContainer = findViewById(R.id.transitionImageContainer)
|
||||
pager2 = attachmentPager
|
||||
directionDetector = createSwipeDirectionDetector()
|
||||
gestureDetector = createGestureDetector()
|
||||
|
||||
attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
onSelectedPositionChanged(position)
|
||||
}
|
||||
})
|
||||
|
||||
swipeDismissHandler = createSwipeToDismissHandler()
|
||||
rootContainer.setOnTouchListener(swipeDismissHandler)
|
||||
rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 }
|
||||
|
||||
scaleDetector = createScaleGestureDetector()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets ->
|
||||
overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom)
|
||||
topInset = insets.systemWindowInsetTop
|
||||
bottomInset = insets.systemWindowInsetBottom
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
fun onSelectedPositionChanged(position: Int) {
|
||||
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let {
|
||||
(it as? BaseViewHolder)?.onSelected(false)
|
||||
}
|
||||
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(position)?.let {
|
||||
(it as? BaseViewHolder)?.onSelected(true)
|
||||
if (it is VideoViewHolder) {
|
||||
it.eventListener = WeakReference(this)
|
||||
}
|
||||
}
|
||||
currentPosition = position
|
||||
overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
attachmentsAdapter.onPause(currentPosition)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
attachmentsAdapter.onResume(currentPosition)
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
// The zoomable view is configured to disallow interception when image is zoomed
|
||||
|
||||
// Check if the overlay is visible, and wants to handle the click
|
||||
if (overlayView?.isVisible == true && overlayView?.dispatchTouchEvent(ev) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev")
|
||||
handleUpDownEvent(ev)
|
||||
|
||||
// Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}")
|
||||
// Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}")
|
||||
// Log.v("ATTACHEMENTS", "wasScaled $wasScaled")
|
||||
if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) {
|
||||
wasScaled = true
|
||||
// Log.v("ATTACHEMENTS", "dispatch to pager")
|
||||
return attachmentPager.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
// Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}")
|
||||
return (if (isScaled()) super.dispatchTouchEvent(ev) else handleTouchIfNotScaled(ev)).also {
|
||||
// Log.v("ATTACHEMENTS", "\n================")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpDownEvent(event: MotionEvent) {
|
||||
// Log.v("ATTACHEMENTS", "handleUpDownEvent $event")
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
handleEventActionUp(event)
|
||||
}
|
||||
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
handleEventActionDown(event)
|
||||
}
|
||||
|
||||
scaleDetector.onTouchEvent(event)
|
||||
gestureDetector.onTouchEvent(event)
|
||||
}
|
||||
|
||||
private fun handleEventActionDown(event: MotionEvent) {
|
||||
swipeDirection = null
|
||||
wasScaled = false
|
||||
attachmentPager.dispatchTouchEvent(event)
|
||||
|
||||
swipeDismissHandler.onTouch(rootContainer, event)
|
||||
isOverlayWasClicked = dispatchOverlayTouch(event)
|
||||
}
|
||||
|
||||
private fun handleEventActionUp(event: MotionEvent) {
|
||||
// wasDoubleTapped = false
|
||||
swipeDismissHandler.onTouch(rootContainer, event)
|
||||
attachmentPager.dispatchTouchEvent(event)
|
||||
isOverlayWasClicked = dispatchOverlayTouch(event)
|
||||
}
|
||||
|
||||
private fun handleSingleTap(event: MotionEvent, isOverlayWasClicked: Boolean) {
|
||||
// TODO if there is no overlay, we should at least toggle system bars?
|
||||
if (overlayView != null && !isOverlayWasClicked) {
|
||||
toggleOverlayViewVisibility()
|
||||
super.dispatchTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleOverlayViewVisibility() {
|
||||
if (systemUiVisibility) {
|
||||
// we hide
|
||||
TransitionManager.beginDelayedTransition(rootContainer)
|
||||
hideSystemUI()
|
||||
overlayView?.isVisible = false
|
||||
} else {
|
||||
// we show
|
||||
TransitionManager.beginDelayedTransition(rootContainer)
|
||||
showSystemUI()
|
||||
overlayView?.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTouchIfNotScaled(event: MotionEvent): Boolean {
|
||||
// Log.v("ATTACHEMENTS", "handleTouchIfNotScaled $event")
|
||||
directionDetector.handleTouchEvent(event)
|
||||
|
||||
return when (swipeDirection) {
|
||||
SwipeDirection.Up, SwipeDirection.Down -> {
|
||||
if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) {
|
||||
swipeDismissHandler.onTouch(rootContainer, event)
|
||||
} else true
|
||||
}
|
||||
SwipeDirection.Left, SwipeDirection.Right -> {
|
||||
attachmentPager.dispatchTouchEvent(event)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) {
|
||||
val alpha = calculateTranslationAlpha(translationY, translationLimit)
|
||||
backgroundView.alpha = alpha
|
||||
dismissContainer.alpha = alpha
|
||||
overlayView?.alpha = alpha
|
||||
}
|
||||
|
||||
private fun dispatchOverlayTouch(event: MotionEvent): Boolean =
|
||||
overlayView
|
||||
?.let { it.isVisible && it.dispatchTouchEvent(event) }
|
||||
?: false
|
||||
|
||||
private fun calculateTranslationAlpha(translationY: Float, translationLimit: Int): Float =
|
||||
1.0f - 1.0f / translationLimit.toFloat() / 4f * abs(translationY)
|
||||
|
||||
private fun createSwipeToDismissHandler()
|
||||
: SwipeToDismissHandler = SwipeToDismissHandler(
|
||||
swipeView = dismissContainer,
|
||||
shouldAnimateDismiss = { shouldAnimateDismiss() },
|
||||
onDismiss = { animateClose() },
|
||||
onSwipeViewMove = ::handleSwipeViewMove)
|
||||
|
||||
private fun createSwipeDirectionDetector() =
|
||||
SwipeDirectionDetector(this) { swipeDirection = it }
|
||||
|
||||
private fun createScaleGestureDetector() =
|
||||
ScaleGestureDetector(this, ScaleGestureDetector.SimpleOnScaleGestureListener())
|
||||
|
||||
private fun createGestureDetector() =
|
||||
GestureDetectorCompat(this, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (isImagePagerIdle) {
|
||||
handleSingleTap(e, isOverlayWasClicked)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
||||
return super.onDoubleTap(e)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onEvent(event: AttachmentEvents) {
|
||||
if (overlayView is AttachmentEventListener) {
|
||||
(overlayView as? AttachmentEventListener)?.onEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun shouldAnimateDismiss(): Boolean = true
|
||||
|
||||
protected open fun animateClose() {
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
finish()
|
||||
}
|
||||
|
||||
fun handle(commands: AttachmentCommands) {
|
||||
(attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder)
|
||||
?.handleCommand(commands)
|
||||
}
|
||||
|
||||
private fun hideSystemUI() {
|
||||
systemUiVisibility = false
|
||||
// Enables regular immersive mode.
|
||||
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
|
||||
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
// Set the content to appear under the system bars so that the
|
||||
// content doesn't resize when the system bars hide and show.
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
// Hide the nav bar and status bar
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||
}
|
||||
|
||||
// Shows the system bars by removing all the flags
|
||||
// except for the ones that make the content appear under the system bars.
|
||||
private fun showSystemUI() {
|
||||
systemUiVisibility = true
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
}
|
||||
}
|
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class AttachmentsAdapter : RecyclerView.Adapter<BaseViewHolder>() {
|
||||
|
||||
var attachmentSourceProvider: AttachmentSourceProvider? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
this.recyclerView = recyclerView
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
this.recyclerView = null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val itemView = inflater.inflate(viewType, parent, false)
|
||||
return when (viewType) {
|
||||
R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView)
|
||||
R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView)
|
||||
R.layout.item_video_attachment -> VideoViewHolder(itemView)
|
||||
else -> UnsupportedViewHolder(itemView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val info = attachmentSourceProvider!!.getAttachmentInfoAt(position)
|
||||
return when (info) {
|
||||
is AttachmentInfo.Image -> R.layout.item_image_attachment
|
||||
is AttachmentInfo.Video -> R.layout.item_video_attachment
|
||||
is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment
|
||||
// is AttachmentInfo.Audio -> TODO()
|
||||
// is AttachmentInfo.File -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return attachmentSourceProvider?.getItemCount() ?: 0
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||
attachmentSourceProvider?.getAttachmentInfoAt(position)?.let {
|
||||
holder.bind(it)
|
||||
when (it) {
|
||||
is AttachmentInfo.Image -> {
|
||||
attachmentSourceProvider?.loadImage((holder as ZoomableImageViewHolder).target, it)
|
||||
}
|
||||
is AttachmentInfo.AnimatedImage -> {
|
||||
attachmentSourceProvider?.loadImage((holder as AnimatedImageViewHolder).target, it)
|
||||
}
|
||||
is AttachmentInfo.Video -> {
|
||||
attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it)
|
||||
}
|
||||
// else -> {
|
||||
// // }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: BaseViewHolder) {
|
||||
holder.onAttached()
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: BaseViewHolder) {
|
||||
holder.onRecycled()
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: BaseViewHolder) {
|
||||
holder.onDetached()
|
||||
}
|
||||
|
||||
fun isScaled(position: Int): Boolean {
|
||||
val holder = recyclerView?.findViewHolderForAdapterPosition(position)
|
||||
if (holder is ZoomableImageViewHolder) {
|
||||
return holder.touchImageView.attacher.scale > 1f
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun onPause(position: Int) {
|
||||
val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder
|
||||
holder?.entersBackground()
|
||||
}
|
||||
|
||||
fun onResume(position: Int) {
|
||||
val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder
|
||||
holder?.entersForeground()
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class BaseViewHolder constructor(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
open fun onRecycled() {
|
||||
boundResourceUid = null
|
||||
}
|
||||
|
||||
open fun onAttached() {}
|
||||
open fun onDetached() {}
|
||||
open fun entersBackground() {}
|
||||
open fun entersForeground() {}
|
||||
open fun onSelected(selected: Boolean) {}
|
||||
|
||||
open fun handleCommand(commands: AttachmentCommands) {}
|
||||
|
||||
var boundResourceUid: String? = null
|
||||
|
||||
open fun bind(attachmentInfo: AttachmentInfo) {
|
||||
boundResourceUid = attachmentInfo.uid
|
||||
}
|
||||
}
|
||||
|
||||
class UnsupportedViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView)
|
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
|
||||
interface ImageLoaderTarget {
|
||||
|
||||
fun contextView(): ImageView
|
||||
|
||||
fun onResourceLoading(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onLoadFailed(uid: String, errorDrawable: Drawable?)
|
||||
|
||||
fun onResourceCleared(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onResourceReady(uid: String, resource: Drawable)
|
||||
}
|
||||
|
||||
internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, private val contextView: ImageView)
|
||||
: ImageLoaderTarget {
|
||||
override fun contextView(): ImageView {
|
||||
return contextView
|
||||
}
|
||||
|
||||
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = true
|
||||
}
|
||||
|
||||
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
}
|
||||
|
||||
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.touchImageView.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(uid: String, resource: Drawable) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
// Glide mess up the view size :/
|
||||
holder.touchImageView.updateLayoutParams {
|
||||
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
height = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
holder.touchImageView.setImageDrawable(resource)
|
||||
if (resource is Animatable) {
|
||||
resource.start()
|
||||
}
|
||||
}
|
||||
|
||||
internal class ZoomableImageTarget(val holder: ZoomableImageViewHolder, private val contextView: ImageView) : ImageLoaderTarget {
|
||||
override fun contextView() = contextView
|
||||
|
||||
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = true
|
||||
}
|
||||
|
||||
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
}
|
||||
|
||||
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.touchImageView.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(uid: String, resource: Drawable) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
// Glide mess up the view size :/
|
||||
holder.touchImageView.updateLayoutParams {
|
||||
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
height = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
holder.touchImageView.setImageDrawable(resource)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
sealed class SwipeDirection {
|
||||
object NotDetected : SwipeDirection()
|
||||
object Up : SwipeDirection()
|
||||
object Down : SwipeDirection()
|
||||
object Left : SwipeDirection()
|
||||
object Right : SwipeDirection()
|
||||
|
||||
companion object {
|
||||
fun fromAngle(angle: Double): SwipeDirection {
|
||||
return when (angle) {
|
||||
in 0.0..45.0 -> Right
|
||||
in 45.0..135.0 -> Up
|
||||
in 135.0..225.0 -> Left
|
||||
in 225.0..315.0 -> Down
|
||||
in 315.0..360.0 -> Right
|
||||
else -> NotDetected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.content.Context
|
||||
import android.view.MotionEvent
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class SwipeDirectionDetector(
|
||||
context: Context,
|
||||
private val onDirectionDetected: (SwipeDirection) -> Unit
|
||||
) {
|
||||
|
||||
private val touchSlop: Int = android.view.ViewConfiguration.get(context).scaledTouchSlop
|
||||
private var startX: Float = 0f
|
||||
private var startY: Float = 0f
|
||||
private var isDetected: Boolean = false
|
||||
|
||||
fun handleTouchEvent(event: MotionEvent) {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
||||
if (!isDetected) {
|
||||
onDirectionDetected(SwipeDirection.NotDetected)
|
||||
}
|
||||
startY = 0.0f
|
||||
startX = startY
|
||||
isDetected = false
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> if (!isDetected && getEventDistance(event) > touchSlop) {
|
||||
isDetected = true
|
||||
onDirectionDetected(getDirection(startX, startY, event.x, event.y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method
|
||||
* returns the direction that an arrow pointing from p1 to p2 would have.
|
||||
*
|
||||
* @param x1 the x position of the first point
|
||||
* @param y1 the y position of the first point
|
||||
* @param x2 the x position of the second point
|
||||
* @param y2 the y position of the second point
|
||||
* @return the direction
|
||||
*/
|
||||
private fun getDirection(x1: Float, y1: Float, x2: Float, y2: Float): SwipeDirection {
|
||||
val angle = getAngle(x1, y1, x2, y2)
|
||||
return SwipeDirection.fromAngle(angle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the angle between two points in the plane (x1,y1) and (x2, y2)
|
||||
* The angle is measured with 0/360 being the X-axis to the right, angles
|
||||
* increase counter clockwise.
|
||||
*
|
||||
* @param x1 the x position of the first point
|
||||
* @param y1 the y position of the first point
|
||||
* @param x2 the x position of the second point
|
||||
* @param y2 the y position of the second point
|
||||
* @return the angle between two points
|
||||
*/
|
||||
private fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double {
|
||||
val rad = Math.atan2((y1 - y2).toDouble(), (x2 - x1).toDouble()) + Math.PI
|
||||
return (rad * 180 / Math.PI + 180) % 360
|
||||
}
|
||||
|
||||
private fun getEventDistance(ev: MotionEvent): Float {
|
||||
val dx = ev.getX(0) - startX
|
||||
val dy = ev.getY(0) - startY
|
||||
return sqrt((dx * dx + dy * dy).toDouble()).toFloat()
|
||||
}
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Rect
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
|
||||
class SwipeToDismissHandler(
|
||||
private val swipeView: View,
|
||||
private val onDismiss: () -> Unit,
|
||||
private val onSwipeViewMove: (translationY: Float, translationLimit: Int) -> Unit,
|
||||
private val shouldAnimateDismiss: () -> Boolean
|
||||
) : View.OnTouchListener {
|
||||
|
||||
companion object {
|
||||
private const val ANIMATION_DURATION = 200L
|
||||
}
|
||||
|
||||
var translationLimit: Int = swipeView.height / 4
|
||||
private var isTracking = false
|
||||
private var startY: Float = 0f
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (swipeView.hitRect.contains(event.x.toInt(), event.y.toInt())) {
|
||||
isTracking = true
|
||||
}
|
||||
startY = event.y
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
if (isTracking) {
|
||||
isTracking = false
|
||||
onTrackingEnd(v.height)
|
||||
}
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (isTracking) {
|
||||
val translationY = event.y - startY
|
||||
swipeView.translationY = translationY
|
||||
onSwipeViewMove(translationY, translationLimit)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun initiateDismissToBottom() {
|
||||
animateTranslation(swipeView.height.toFloat())
|
||||
}
|
||||
|
||||
private fun onTrackingEnd(parentHeight: Int) {
|
||||
val animateTo = when {
|
||||
swipeView.translationY < -translationLimit -> -parentHeight.toFloat()
|
||||
swipeView.translationY > translationLimit -> parentHeight.toFloat()
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
if (animateTo != 0f && !shouldAnimateDismiss()) {
|
||||
onDismiss()
|
||||
} else {
|
||||
animateTranslation(animateTo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateTranslation(translationTo: Float) {
|
||||
swipeView.animate()
|
||||
.translationY(translationTo)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.setInterpolator(AccelerateInterpolator())
|
||||
.setUpdateListener { onSwipeViewMove(swipeView.translationY, translationLimit) }
|
||||
.setAnimatorListener(onAnimationEnd = {
|
||||
if (translationTo != 0f) {
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// remove the update listener, otherwise it will be saved on the next animation execution:
|
||||
swipeView.animate().setUpdateListener(null)
|
||||
})
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ViewPropertyAnimator.setAnimatorListener(
|
||||
onAnimationEnd: ((Animator?) -> Unit)? = null,
|
||||
onAnimationStart: ((Animator?) -> Unit)? = null
|
||||
) = this.setListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
onAnimationEnd?.invoke(animation)
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
onAnimationStart?.invoke(animation)
|
||||
}
|
||||
})
|
||||
|
||||
internal val View?.hitRect: Rect
|
||||
get() = Rect().also { this?.getHitRect(it) }
|
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.isVisible
|
||||
import java.io.File
|
||||
|
||||
interface VideoLoaderTarget {
|
||||
fun contextView(): ImageView
|
||||
|
||||
fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?)
|
||||
|
||||
fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onThumbnailResourceReady(uid: String, resource: Drawable)
|
||||
|
||||
fun onVideoFileLoading(uid: String)
|
||||
fun onVideoFileLoadFailed(uid: String)
|
||||
fun onVideoFileReady(uid: String, file: File)
|
||||
}
|
||||
|
||||
internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget {
|
||||
override fun contextView(): ImageView = contextView
|
||||
|
||||
override fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onThumbnailResourceReady(uid: String, resource: Drawable) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.setImageDrawable(resource)
|
||||
}
|
||||
|
||||
override fun onVideoFileLoading(uid: String) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.isVisible = true
|
||||
holder.loaderProgressBar.isVisible = true
|
||||
holder.videoView.isVisible = false
|
||||
}
|
||||
|
||||
override fun onVideoFileLoadFailed(uid: String) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.videoFileLoadError()
|
||||
}
|
||||
|
||||
override fun onVideoFileReady(uid: String, file: File) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.isVisible = false
|
||||
holder.loaderProgressBar.isVisible = false
|
||||
holder.videoView.isVisible = true
|
||||
holder.videoReady(file)
|
||||
}
|
||||
}
|
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.VideoView
|
||||
import androidx.core.view.isVisible
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// TODO, it would be probably better to use a unique media player
|
||||
// for better customization and control
|
||||
// But for now VideoView is enough, it released player when detached, we use a timer to update progress
|
||||
class VideoViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView) {
|
||||
|
||||
private var isSelected = false
|
||||
private var mVideoPath: String? = null
|
||||
private var progressDisposable: Disposable? = null
|
||||
private var progress: Int = 0
|
||||
private var wasPaused = false
|
||||
|
||||
var eventListener: WeakReference<AttachmentEventListener>? = null
|
||||
|
||||
val thumbnailImage: ImageView = itemView.findViewById(R.id.videoThumbnailImage)
|
||||
val videoView: VideoView = itemView.findViewById(R.id.videoView)
|
||||
val loaderProgressBar: ProgressBar = itemView.findViewById(R.id.videoLoaderProgress)
|
||||
val videoControlIcon: ImageView = itemView.findViewById(R.id.videoControlIcon)
|
||||
val errorTextView: TextView = itemView.findViewById(R.id.videoMediaViewerErrorView)
|
||||
|
||||
internal val target = DefaultVideoLoaderTarget(this, thumbnailImage)
|
||||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
mVideoPath = null
|
||||
}
|
||||
|
||||
fun videoReady(file: File) {
|
||||
mVideoPath = file.path
|
||||
if (isSelected) {
|
||||
startPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
fun videoFileLoadError() {
|
||||
}
|
||||
|
||||
override fun entersBackground() {
|
||||
if (videoView.isPlaying) {
|
||||
progress = videoView.currentPosition
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
videoView.stopPlayback()
|
||||
videoView.pause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun entersForeground() {
|
||||
onSelected(isSelected)
|
||||
}
|
||||
|
||||
override fun onSelected(selected: Boolean) {
|
||||
if (!selected) {
|
||||
if (videoView.isPlaying) {
|
||||
progress = videoView.currentPosition
|
||||
videoView.stopPlayback()
|
||||
} else {
|
||||
progress = 0
|
||||
}
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
} else {
|
||||
if (mVideoPath != null) {
|
||||
startPlaying()
|
||||
}
|
||||
}
|
||||
isSelected = true
|
||||
}
|
||||
|
||||
private fun startPlaying() {
|
||||
thumbnailImage.isVisible = false
|
||||
loaderProgressBar.isVisible = false
|
||||
videoView.isVisible = true
|
||||
|
||||
videoView.setOnPreparedListener {
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS)
|
||||
.timeInterval()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
val duration = videoView.duration
|
||||
val progress = videoView.currentPosition
|
||||
val isPlaying = videoView.isPlaying
|
||||
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
|
||||
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
||||
}
|
||||
}
|
||||
|
||||
videoView.setVideoPath(mVideoPath)
|
||||
if (!wasPaused) {
|
||||
videoView.start()
|
||||
if (progress > 0) {
|
||||
videoView.seekTo(progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleCommand(commands: AttachmentCommands) {
|
||||
if (!isSelected) return
|
||||
when (commands) {
|
||||
AttachmentCommands.StartVideo -> {
|
||||
wasPaused = false
|
||||
videoView.start()
|
||||
}
|
||||
AttachmentCommands.PauseVideo -> {
|
||||
wasPaused = true
|
||||
videoView.pause()
|
||||
}
|
||||
is AttachmentCommands.SeekTo -> {
|
||||
val duration = videoView.duration
|
||||
if (duration > 0) {
|
||||
val seekDuration = duration * (commands.percentProgress / 100f)
|
||||
videoView.seekTo(seekDuration.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(attachmentInfo: AttachmentInfo) {
|
||||
super.bind(attachmentInfo)
|
||||
progress = 0
|
||||
wasPaused = false
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
|
||||
class ZoomableImageViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView) {
|
||||
|
||||
val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView)
|
||||
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
|
||||
|
||||
init {
|
||||
touchImageView.setAllowParentInterceptOnEdge(false)
|
||||
touchImageView.setOnScaleChangeListener { scaleFactor, _, _ ->
|
||||
// Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor")
|
||||
// It's a bit annoying but when you pitch down the scaling
|
||||
// is not exactly one :/
|
||||
touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f)
|
||||
}
|
||||
touchImageView.setScale(1.0f, true)
|
||||
touchImageView.setAllowParentInterceptOnEdge(true)
|
||||
}
|
||||
|
||||
internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView)
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/rootContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".AttachmentViewerActivity">
|
||||
|
||||
<View
|
||||
android:id="@+id/backgroundView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="1"
|
||||
android:background="@android:color/black" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/dismissContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/transitionImageContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="UselessParent"
|
||||
tools:visibility="invisible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/transitionImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/attachmentPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:visibility="visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_centerInParent="true"
|
||||
android:id="@+id/imageLoaderProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:visibility="visible"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
android:id="@+id/touchImageView"
|
||||
android:visibility="visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_centerInParent="true"
|
||||
android:id="@+id/imageLoaderProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:visibility="visible"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoThumbnailImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:scaleType="centerInside" />
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoControlIcon"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_centerInParent="true"
|
||||
android:id="@+id/videoLoaderProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:visibility="invisible"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videoMediaViewerErrorView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_margin="16dp"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
tools:text="Error"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</RelativeLayout>
|
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/design_default_color_primary">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/testPage"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1"
|
||||
android:textSize="80sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
|
||||
</RelativeLayout>
|
@@ -1,7 +1,7 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.3.72'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
@@ -10,12 +10,13 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
// Warning: 3.6.3 leads to infinite gradle builds. Stick to 3.5.3 for the moment
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.google.gms:google-services:4.3.2'
|
||||
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.9.5'
|
||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
@@ -38,6 +39,8 @@ allprojects {
|
||||
includeGroupByRegex "com\\.github\\.yalantis"
|
||||
// JsonViewer
|
||||
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
||||
// PhotoView
|
||||
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||
}
|
||||
}
|
||||
maven {
|
||||
|
92
docs/identity_server.md
Normal file
92
docs/identity_server.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Identity server
|
||||
|
||||
Issue: #607
|
||||
PR: #1354
|
||||
|
||||
## Introduction
|
||||
Identity Servers support contact discovery on Matrix by letting people look up Third Party Identifiers to see if the owner has publicly linked them with their Matrix ID.
|
||||
|
||||
## Implementation
|
||||
|
||||
The current implementation was Inspired by the code from Riot-Android.
|
||||
|
||||
Difference though (list not exhaustive):
|
||||
- Only API v2 is supported (see https://matrix.org/docs/spec/identity_service/latest)
|
||||
- Homeserver has to be up to date to support binding (Versions.isLoginAndRegistrationSupportedBySdk() has to return true)
|
||||
- The SDK managed the session and client secret when binding ThreePid. Those data are not exposed to the client.
|
||||
- The SDK supports incremental sendAttempt (this is not used by RiotX)
|
||||
- The "Continue" button is now under the information, and not as the same place that the checkbox
|
||||
- The app can cancel a binding. Current data are erased from DB.
|
||||
- The API (IdentityService) is improved.
|
||||
- A new DB to store data related to the identity server management.
|
||||
|
||||
Missing features (list not exhaustive):
|
||||
- Invite by 3Pid (will be in a dedicated PR)
|
||||
- Add email or phone to account (not P1, can be done on Riot-Web)
|
||||
- List email and phone of the account (could be done in a dedicated PR)
|
||||
- Search contact (not P1)
|
||||
- Logout from identity server when user sign out or deactivate his account.
|
||||
|
||||
## Related MSCs
|
||||
The list can be found here: https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4
|
||||
|
||||
## Steps and requirements
|
||||
|
||||
- Only one identity server by account can be set. The user's choice is stored in account data with key `m.identity_server`. But every clients will managed its own token to log in to the identity server
|
||||
```json
|
||||
{
|
||||
"type": "m.identity_server",
|
||||
"content": {
|
||||
"base_url": "https://matrix.org"
|
||||
}
|
||||
}
|
||||
```
|
||||
- The accepted terms are stored in the account data:
|
||||
```json
|
||||
{
|
||||
"type": "m.accepted_terms",
|
||||
"content": {
|
||||
"accepted": [
|
||||
"https://vector.im/identity-server-privacy-notice-1"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Default identity server URL, from Wellknown data is proposed to the user.
|
||||
- Identity server can be set
|
||||
- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) RiotX should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever)
|
||||
- Registration to the identity server is managed with an openId token
|
||||
- Terms of service can be accepted when configuring the identity server.
|
||||
- Terms of service can be accepted after, if they change.
|
||||
- Identity server can be modified
|
||||
- Identity server can be disconnected with a warning dialog, with special content if there are current bound 3pid on this identity server.
|
||||
- Email can be bound
|
||||
- Email can be unbound
|
||||
- Phone can be bound
|
||||
- Phone can be unbound
|
||||
- Look up can be performed, to get matrixIds from local contact book (phone and email): Android permission correctly handled (not done yet)
|
||||
- Look up pepper can be updated if it is rotated on the identity server
|
||||
- Invitation using 3PID can be done (See #548) (not done yet)
|
||||
- Homeserver access-token will never be sent to an identity server
|
||||
- When user sign-out: logout from the identity server if any.
|
||||
- When user deactivate account: logout from the identity server if any.
|
||||
|
||||
## Screens
|
||||
|
||||
### Settings
|
||||
|
||||
Identity server settings can be accessed from the internal setting of the application, both from "Discovery" section and from identity detail section.
|
||||
|
||||
### Discovery screen
|
||||
|
||||
This screen displays the identity server configuration and the binding of the user's ThreePid (email and msisdn). This is the main screen of the feature.
|
||||
|
||||
### Set identity server screen
|
||||
|
||||
This screen is a form to set a new identity server URL
|
||||
|
||||
## Ref:
|
||||
- https://matrix.org/blog/2019/09/27/privacy-improvements-in-synapse-1-4-and-riot-1-4 is a good summary of the role of an Identity server and the proper way to configure and use it in respect to the privacy and the consent of the user.
|
||||
- API documentation: https://matrix.org/docs/spec/identity_service/latest
|
||||
- vector.im TOS: https://vector.im/identity-server-privacy-notice
|
@@ -59,6 +59,12 @@ It's recommended to run tests using an Android Emulator and not a real device. F
|
||||
|
||||
You can run all the tests in the `androidTest` folders.
|
||||
|
||||
It can be done using this command:
|
||||
|
||||
```bash
|
||||
./gradlew vector:connectedAndroidTest matrix-sdk-android:connectedAndroidTest
|
||||
```
|
||||
|
||||
## Stop Synapse
|
||||
|
||||
To stop Synapse, you can run the following commands:
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver.
|
||||
|
||||
## Sign up flows
|
||||
## Sign in flows
|
||||
|
||||
### Get the flow
|
||||
|
||||
@@ -58,7 +58,7 @@ We get credential (200)
|
||||
```json
|
||||
{
|
||||
"user_id": "@alice:matrix.org",
|
||||
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
|
||||
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lr",
|
||||
"home_server": "matrix.org",
|
||||
"device_id": "GTVREDALBF",
|
||||
"well_known": {
|
||||
@@ -117,7 +117,7 @@ We get the credentials (200)
|
||||
```json
|
||||
{
|
||||
"user_id": "@alice:matrix.org",
|
||||
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg",
|
||||
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3Jnfrfdegfszsefddvf",
|
||||
"home_server": "matrix.org",
|
||||
"device_id": "WBSREDASND",
|
||||
"well_known": {
|
||||
@@ -145,12 +145,59 @@ Not supported yet in RiotX
|
||||
"flows": [
|
||||
{
|
||||
"type": "m.login.sso"
|
||||
},
|
||||
{
|
||||
"type": "m.login.token"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
In this case, the user can click on "Sign in with SSO" and the web screen will be displayed on the page `https://homeserver.with.sso/_matrix/static/client/login/` and the credentials will be passed back to the native code through the JS bridge
|
||||
In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page
|
||||
|
||||
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=riotx%3A%2F%2Friotx
|
||||
|
||||
The parameter `redirectUrl` is set to `riotx://riotx`.
|
||||
|
||||
ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs)
|
||||
|
||||
The browser will then take care of the SSO login, which may include creating a third party account, entering an email, settings a display name, or any other possibilities.
|
||||
|
||||
During the process, user may be asked to validate an email by clicking on a link it contains. The link has to be opened in the browser which initiates the authentication. This is why we cannot use WebView anymore.
|
||||
|
||||
Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken`
|
||||
|
||||
> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
|
||||
|
||||
This navigation is intercepted by RiotX by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
|
||||
|
||||
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "m.login.token",
|
||||
"token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"
|
||||
}
|
||||
```
|
||||
|
||||
We get the credentials (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"user_id": "@alice:homeserver.with.sso",
|
||||
"access_token": "MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVyIGtleQowMDEwY2lkIGdlbiA9IDEKMDAyY2NpZCB1c2",
|
||||
"home_server": "homeserver.with.sso",
|
||||
"device_id": "DETBTVAHCH",
|
||||
"well_known": {
|
||||
"m.homeserver": {
|
||||
"base_url": "https:\/\/homeserver.with.sso\/"
|
||||
},
|
||||
"m.identity_server": {
|
||||
"base_url": "https:\/\/vector.im"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Reset password
|
||||
|
||||
|
425
docs/voip_signaling.md
Normal file
425
docs/voip_signaling.md
Normal file
@@ -0,0 +1,425 @@
|
||||
Useful links:
|
||||
- https://codelabs.developers.google.com/codelabs/webrtc-web/#0
|
||||
- http://webrtc.github.io/webrtc-org/native-code/android/
|
||||
|
||||
|
||||
╔════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║A] Placing a call offer ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════╝
|
||||
|
||||
|
||||
|
||||
┌───────────────┐
|
||||
│ Matrix │
|
||||
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
│
|
||||
│
|
||||
│
|
||||
┌─────────────────┐ │ ┌───────────────────────────┐ ┌─────────────────┐
|
||||
│ Caller │ │ Signaling Room │ │ │ Callee │
|
||||
└─────────────────┘ │ ├───────────────────────────┤ └─────────────────┘
|
||||
┌────┐ │ │ │
|
||||
│ 3 │ │ │ ┌────────────────────┐ │
|
||||
┌─────────────────┐──────┴────┴──────────────────────────┼─▶│ m.call.invite │ │ │ ┌─────────────────┐
|
||||
│ │ │ │ │ mx event │ │ │ │
|
||||
│ │ │ └────────────────────┘ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ Riot.im │ │ │ │ │ Riot.im │
|
||||
┌──│ App │ │ │ │ │ App │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ └─────────────────┘ │ │ │ └─────────────────┘
|
||||
┌────┤ ▲ │ │ │
|
||||
│ 1 │ ├────┐ │ └───────────────────────────┘
|
||||
└────┤ │ 2 │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
|
||||
│ ┌──┴────┴─────────┐ ┌─────────────────┐
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ │ WebRtc │ │ WebRtc │
|
||||
└─▶│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
┌────┐
|
||||
│ 1 │ The Caller app get access to system resources (camera, mic), eventually stun/turn servers, define some
|
||||
└────┘ constrains (video quality, format) and pass it to WebRtc in order to create a Peer Call offer
|
||||
|
||||
┌────┐
|
||||
│ 2 │ The WebRtc layer creates a call Offer (sdp) that needs to be sent to callee
|
||||
└────┘
|
||||
|
||||
┌────┐ The app layer, takes the webrtc offer, encapsulate it in a matrix event adds a callId and send it to the other
|
||||
│ 3 │ user via the room
|
||||
└────┘
|
||||
┌──────────────┐
|
||||
│ mx event │
|
||||
├──────────────┴────────┐
|
||||
│ type: m.call.invite │
|
||||
│ + callId │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ webrtc sdp │ │
|
||||
│ └──────────────────┘ │
|
||||
└───────────────────────┘
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╔════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║B] Sending connection establishment info ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════╝
|
||||
|
||||
|
||||
|
||||
┌───────────────┐
|
||||
│ Matrix │
|
||||
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
│
|
||||
│
|
||||
│
|
||||
┌─────────────────┐ │ ┌───────────────────────────┐ ┌─────────────────┐
|
||||
│ Caller │ │ Signaling Room │ │ │ Callee │
|
||||
└─────────────────┘ │ ├───────────────────────────┤ └─────────────────┘
|
||||
│ ┌────────────────────┐ │ │
|
||||
│ │ │ m.call.invite │ │
|
||||
┌─────────────────┐ │ │ mx event │ │ │ ┌─────────────────┐
|
||||
│ │ ┌────┐ │ │ └────────────────────┘ │ │ │
|
||||
│ │ │ 3 │ │ ┌────────────────────┐ │ │ │ │
|
||||
│ │──────┴────┴───────┼──────────────────┼─▶│ m.call.candidates │ │ │ │
|
||||
│ Riot.im │ │ │ mx event │ │ │ │ Riot.im │
|
||||
│ App │ │ │ └────────────────────┘ │ │ App │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
└─────────────────┘ │ │ │ └─────────────────┘
|
||||
▲ │ │ │
|
||||
├────┐ │ └───────────────────────────┘
|
||||
│ 2 │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
|
||||
┌───────┴────┴────┐ ┌─────────────────┐
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ WebRtc │ ┌───────────────┐ │ WebRtc │
|
||||
│ │ │ Stun / Turn │ │ │
|
||||
│ │ ├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │
|
||||
│ │ │ │ │
|
||||
└─────────────────┘ │ └─────────────────┘
|
||||
▲ │
|
||||
│ │
|
||||
└──────────┬────┬───────────▶ │
|
||||
┌───────────────┐ │ 1 │ │
|
||||
│ │ └────┘ │
|
||||
│ Network Stack │ │
|
||||
│ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
|
||||
│ │
|
||||
└───────────────┘
|
||||
|
||||
|
||||
|
||||
|
||||
┌────┐
|
||||
│ 1 │ The WebRtc layer gathers information on how it can be reach by the other peer directly (Ice candidates)
|
||||
└────┘
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│candidate:1 1 tcp 1518149375 127.0.0.1 35990 typ host │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│candidate:2 1 UDP 2130706431 192.168.1.102 1816 typ host │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌────┐
|
||||
│ 2 │ The WebRTC layer notifies the App layer when it finds new Ice Candidates
|
||||
└────┘
|
||||
|
||||
|
||||
|
||||
┌────┐ The app layer, takes the ice candidates, encapsulate them in one or several matrix event adds the callId and
|
||||
│ 3 │ send it to the other user via the room
|
||||
└────┘
|
||||
┌──────────────┐
|
||||
│ mx event │
|
||||
├──────────────┴────────────────────────┐
|
||||
│ type: m.call.candidates │
|
||||
│ │
|
||||
│ +CallId │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ice candidate sdp │ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ice candidate sdp │ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ice candidate sdp │ │
|
||||
│ └──────────────────┘ │
|
||||
└───────────────────────────────────────┘
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╔════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║C] Receiving a call offer ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════╝
|
||||
|
||||
|
||||
|
||||
┌───────────────┐
|
||||
│ Matrix │
|
||||
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
│
|
||||
│ ┌─────────────────┐
|
||||
│ │ Callee │
|
||||
┌─────────────────┐ │ ┌───────────────────────────┐ └─────────────────┘
|
||||
│ Caller │ │ Signaling Room │ │
|
||||
└─────────────────┘ │ ├───────────────────────────┤
|
||||
│ ┌────────────────────┐ │ │ ┌─────────────────┐
|
||||
│ │ │ m.call.invite │───┼────────────────────────────┬────┬───▶│ │
|
||||
┌─────────────────┐ │ │ mx event │ │ │ │ 1 │ │ │
|
||||
│ │ │ │ └────────────────────┘ │ └────┘ │ │
|
||||
│ │ │ ┌────────────────────┐ │ │ │ Riot.im │
|
||||
│ │ │ │ │ m.call.candidates │ │ │ App │
|
||||
│ Riot.im │ │ │ mx event │ │ │ │ │
|
||||
│ App │ │ │ └────────────────────┘ │ │ │
|
||||
│ │ │ ┌────────────────────┐◀──┼─────────────────┼───┬────┬───────────┤ │
|
||||
│ │◀──────────────────┼──────────────────┼──│ m.call.answer │ │ │ 4 │ └──┬──────────────┘
|
||||
│ │ │ │ mx event │ │ │ └────┘ ├────┐ ▲
|
||||
└────┬────────────┘ │ │ └────────────────────┘ │ │ 2 │ ├────┐
|
||||
│ │ │ │ ├────┘ │ 3 │
|
||||
│ │ └───────────────────────────┘ ┌──▼─────────┴────┤
|
||||
┌────▼────────────┐ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │
|
||||
│ │ │ │
|
||||
│ │ │ WebRtc │
|
||||
│ WebRtc │ │ ┌──┴─────────────────┐
|
||||
│ │ │ │ caller offer │
|
||||
┌──────────┴─────────┐ │ │ └──┬─────────────────┘
|
||||
│ callee answer │ │ └─────────────────┘
|
||||
└────────────────────┴───────┘
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
┌────┐
|
||||
│ 1 │ Bob receives a call.invite event in a room, then creates a WebRTC peer connection object
|
||||
└────┘
|
||||
|
||||
┌────┐
|
||||
│ 2 │ The encapsulated call offer sdp from the mx event is transmitted to WebRTC
|
||||
└────┘
|
||||
|
||||
┌────┐
|
||||
│ 3 │ WebRTC then creates a call answer for the offer and send it back to app layer
|
||||
└────┘
|
||||
|
||||
|
||||
┌────┐ The app layer, takes the webrtc answer, encapsulate it in a matrix event adds a callId and send it to the
|
||||
│ 3 │ other user via the room
|
||||
└────┘
|
||||
┌──────────────┐
|
||||
│ mx event │
|
||||
├──────────────┴────────┐
|
||||
│ type: m.call.answer │
|
||||
│ + callId │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ webrtc sdp │ │
|
||||
│ └──────────────────┘ │
|
||||
└───────────────────────┘
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╔════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║D] Callee sends connection establishment info ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════╝
|
||||
|
||||
|
||||
|
||||
┌───────────────┐
|
||||
│ Matrix │
|
||||
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
│
|
||||
│
|
||||
│
|
||||
┌─────────────────┐ │ ┌───────────────────────────┐ ┌─────────────────┐
|
||||
│ Caller │ │ Signaling Room │ │ │ Callee │
|
||||
└─────────────────┘ │ ├───────────────────────────┤ └─────────────────┘
|
||||
│ ┌────────────────────┐ │ │
|
||||
│ │ │ m.call.invite │ │
|
||||
┌─────────────────┐ │ │ mx event │ │ │ ┌─────────────────┐
|
||||
│ │ │ │ └────────────────────┘ │ │ │
|
||||
│ │ │ ┌────────────────────┐ │ │ │ │
|
||||
│ │ │ │ │ m.call.candidates │ │ │ │
|
||||
│ Riot.im │ │ │ mx event │ │ │ │ Riot.im │
|
||||
│ App │ │ │ └────────────────────┘ │ ┌────┐ │ App │
|
||||
│ │ │ ┌────────────────────┐ │ │ │ 3 │ │ │
|
||||
│ │◀──────────────────┼┐ │ │ m.call.answer │ │ ┌───────┴────┴────────│ │
|
||||
│ │ │ │ │ mx event │ │ ││ │ │
|
||||
└─────────────────┘ ││ │ └────────────────────┘ │ │ └─────────────────┘
|
||||
│ │ │ ┌────────────────────┐ │ ││ ▲
|
||||
│ │└─────────────────┼──│ m.call.candidates │ │ │ ├────┐
|
||||
▼ │ │ mx event │◀──┼────────────────┘│ │ 2 │
|
||||
┌─────────────────┐ │ │ └────────────────────┘ │ ┌────┴────┴───────┐
|
||||
│ │ └───────────────────────────┘ │ │ │
|
||||
│ │ │ │ │
|
||||
│ WebRtc │ │ │ WebRtc │
|
||||
│ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ┌───┴────────────────┐
|
||||
│ │ │ │ caller offer │
|
||||
┌────────┴───────────┐ │ │ └───┬────────────────┘
|
||||
│ callee answer ├─────┘ ┌───────────────┐ └─────────────────┘
|
||||
├────────────────────┤ │ Stun / Turn │ ▲
|
||||
│ callee ice │ ├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ┌────┐ │
|
||||
│ candidates │ │ │ 1 │ │
|
||||
└────────────────────┘ │ ├────┴──┴───────┐
|
||||
│ │ │
|
||||
│ │ Network Stack │
|
||||
│◀─────────────────────┤ │
|
||||
│ │ │
|
||||
│ └───────────────┘
|
||||
│
|
||||
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
|
||||
|
||||
|
||||
┌────┐
|
||||
│ 1 │ The WebRtc layer gathers information on how it can be reach by the other peer directly (Ice candidates)
|
||||
└────┘
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│candidate:1 1 tcp 1518149375 127.0.0.1 35990 typ host │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│candidate:2 1 UDP 2130706431 192.168.1.102 1816 typ host │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌────┐
|
||||
│ 2 │ The WebRTC layer notifies the App layer when it finds new Ice Candidates
|
||||
└────┘
|
||||
|
||||
|
||||
|
||||
┌────┐ The app layer, takes the ice candidates, encapsulate them in one or several matrix event adds the callId and
|
||||
│ 3 │ send it to the other user via the room
|
||||
└────┘
|
||||
┌──────────────┐
|
||||
│ mx event │
|
||||
├──────────────┴────────────────────────┐
|
||||
│ type: m.call.candidates │
|
||||
│ │
|
||||
│ +CallId │
|
||||
│ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ice candidate sdp │ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ice candidate sdp │ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌──────────────────┐ │
|
||||
│ │ice candidate sdp │ │
|
||||
│ └──────────────────┘ │
|
||||
└───────────────────────────────────────┘
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
╔════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║D] Caller Callee connection ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════╝
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
┌───────────────┐
|
||||
┌─────────────────┐ │ Matrix │ ┌─────────────────┐
|
||||
│ Caller │ ├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Callee │
|
||||
└─────────────────┘ │ └─────────────────┘
|
||||
│
|
||||
│
|
||||
┌─────────────────┐ │ ┌─────────────────┐
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ Riot.im │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Riot.im │
|
||||
│ App │ │ App │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
┌───────────────┐
|
||||
│ Internet │
|
||||
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
┌─────────────────┐ │ ┌─────────────────┐
|
||||
│ │ │ │ │
|
||||
│ ├───────────────────────────────────────────────────────────────────────────────────┴─────────────────────┤ │
|
||||
│ WebRtc │█████████████████████████████████████████████████████████████████████████████████████████████████████████│ WebRtc │
|
||||
┌─────────────┴──────┐ ├────────────────────────────────────────┬──────────────────────────┬───────────────┬─────────────────────┤ ┌─────┴──────────────┐
|
||||
│ callee answer │ │ │ │ Video / Audio Stream │ │ │ caller offer │
|
||||
├────────────────────┤ │ └──────────────────────────┘ │ │ ├────────────────────┤
|
||||
│ callee ice ├──────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └───────────┤ caller ice │
|
||||
│ candidates │ │ candidates │
|
||||
└────────────────────┘ └────────────────────┘
|
||||
|
||||
|
||||
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ │░
|
||||
│ If connection is impossible (firewall), and a turn │░
|
||||
│server is available, connection could happen through │░
|
||||
│ a relay │░
|
||||
│ │░
|
||||
└─────────────────────────────────────────────────────┘░
|
||||
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
||||
|
||||
|
||||
┌───────────────┐
|
||||
│ Internet │
|
||||
└─┬─────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
┌─────────────────┐ │ ┌─────────────────┐
|
||||
│ │ │ ┌─────────────────────────┐ │ │
|
||||
│ ├───────────────────────────────────────┐│ │ │ │ │
|
||||
│ WebRtc │███████████████████████████████████████││ │ │ WebRtc │
|
||||
│ ├───────────────────────────────────────┘│ │ │ │ │
|
||||
│ │ ┌────────┴─────────────────┐ │ Relay │┌─────────────────────────────────────┤ │
|
||||
┌───────────────┴────┐ │ │ Video / Audio Stream │ │ ││█████████████████████████████████████│ ┌───────┴────────────┐
|
||||
│ callee answer ├────────────┘ └────────┬─────────────────┘ │ │└─────────────────────────────────────┴─────────┤ caller offer │
|
||||
├────────────────────┤ │ │ │ ├────────────────────┤
|
||||
│ callee ice │ │ │ │ │ caller ice │
|
||||
│ candidates │ └─────────────────────────┘ │ │ candidates │
|
||||
└────────────────────┘ │ └────────────────────┘
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
@@ -8,7 +8,7 @@
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx8192m
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Fri Sep 27 10:10:35 CEST 2019
|
||||
#Thu Jul 02 12:33:07 CEST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
|
@@ -6,7 +6,7 @@ android {
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
@@ -39,7 +39,7 @@ dependencies {
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
|
||||
|
||||
// Logging
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
@@ -16,11 +16,15 @@
|
||||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import android.net.Uri
|
||||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.notification.RoomNotificationState
|
||||
@@ -61,13 +65,20 @@ class RxRoom(private val room: Room) {
|
||||
}
|
||||
}
|
||||
|
||||
fun liveStateEvent(eventType: String, stateKey: String): Observable<Optional<Event>> {
|
||||
fun liveStateEvent(eventType: String, stateKey: QueryStringValue): Observable<Optional<Event>> {
|
||||
return room.getStateEventLive(eventType, stateKey).asObservable()
|
||||
.startWithCallable {
|
||||
room.getStateEvent(eventType, stateKey).toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
fun liveStateEvents(eventTypes: Set<String>): Observable<List<Event>> {
|
||||
return room.getStateEventsLive(eventTypes).asObservable()
|
||||
.startWithCallable {
|
||||
room.getStateEvents(eventTypes)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveReadMarker(): Observable<Optional<String>> {
|
||||
return room.getReadMarkerLive().asObservable()
|
||||
}
|
||||
@@ -100,6 +111,34 @@ class RxRoom(private val room: Room) {
|
||||
fun invite(userId: String, reason: String? = null): Completable = completableBuilder<Unit> {
|
||||
room.invite(userId, reason, it)
|
||||
}
|
||||
|
||||
fun invite3pid(threePid: ThreePid): Completable = completableBuilder<Unit> {
|
||||
room.invite3pid(threePid, it)
|
||||
}
|
||||
|
||||
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
||||
room.updateTopic(topic, it)
|
||||
}
|
||||
|
||||
fun updateName(name: String): Completable = completableBuilder<Unit> {
|
||||
room.updateName(name, it)
|
||||
}
|
||||
|
||||
fun addRoomAlias(alias: String): Completable = completableBuilder<Unit> {
|
||||
room.addRoomAlias(alias, it)
|
||||
}
|
||||
|
||||
fun updateCanonicalAlias(alias: String): Completable = completableBuilder<Unit> {
|
||||
room.updateCanonicalAlias(alias, it)
|
||||
}
|
||||
|
||||
fun updateHistoryReadability(readability: RoomHistoryVisibility): Completable = completableBuilder<Unit> {
|
||||
room.updateHistoryReadability(readability, it)
|
||||
}
|
||||
|
||||
fun updateAvatar(avatarUri: Uri, fileName: String): Completable = completableBuilder<Unit> {
|
||||
room.updateAvatar(avatarUri, fileName, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun Room.rx(): RxRoom {
|
||||
|
@@ -17,25 +17,36 @@
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.pushers.Pusher
|
||||
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.api.session.widgets.model.Widget
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function3
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
@@ -53,10 +64,10 @@ class RxSession(private val session: Session) {
|
||||
}
|
||||
}
|
||||
|
||||
fun liveBreadcrumbs(): Observable<List<RoomSummary>> {
|
||||
return session.getBreadcrumbsLive().asObservable()
|
||||
fun liveBreadcrumbs(queryParams: RoomSummaryQueryParams): Observable<List<RoomSummary>> {
|
||||
return session.getBreadcrumbsLive(queryParams).asObservable()
|
||||
.startWithCallable {
|
||||
session.getBreadcrumbs()
|
||||
session.getBreadcrumbs(queryParams)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +105,11 @@ class RxSession(private val session: Session) {
|
||||
return session.getPagedUsersLive(filter, excludedUserIds).asObservable()
|
||||
}
|
||||
|
||||
fun liveThreePIds(refreshData: Boolean): Observable<List<ThreePid>> {
|
||||
return session.getThreePidsLive(refreshData).asObservable()
|
||||
.startWithCallable { session.getThreePids() }
|
||||
}
|
||||
|
||||
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
|
||||
session.createRoom(roomParams, it)
|
||||
}
|
||||
@@ -145,6 +161,54 @@ class RxSession(private val session: Session) {
|
||||
session.getAccountDataEvents(types)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomWidgets(
|
||||
roomId: String,
|
||||
widgetId: QueryStringValue,
|
||||
widgetTypes: Set<String>? = null,
|
||||
excludedTypes: Set<String>? = null
|
||||
): Observable<List<Widget>> {
|
||||
return session.widgetService().getRoomWidgetsLive(roomId, widgetId, widgetTypes, excludedTypes).asObservable()
|
||||
.startWithCallable {
|
||||
session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomChangeMembershipState(): Observable<Map<String, ChangeMembershipState>> {
|
||||
return session.getChangeMembershipsLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveSecretSynchronisationInfo(): Observable<SecretsSynchronisationInfo> {
|
||||
return Observable.combineLatest<List<UserAccountData>, Optional<MXCrossSigningInfo>, Optional<PrivateKeysInfo>, SecretsSynchronisationInfo>(
|
||||
liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),
|
||||
liveCrossSigningInfo(session.myUserId),
|
||||
liveCrossSigningPrivateKeys(),
|
||||
Function3 { _, crossSigningInfo, pInfo ->
|
||||
// first check if 4S is already setup
|
||||
val is4SSetup = session.sharedSecretStorageService.isRecoverySetup()
|
||||
val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null
|
||||
val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true
|
||||
val allPrivateKeysKnown = pInfo.getOrNull()?.allKnown().orFalse()
|
||||
|
||||
val keysBackupService = session.cryptoService().keysBackupService()
|
||||
val currentBackupVersion = keysBackupService.currentBackupVersion
|
||||
val megolmBackupAvailable = currentBackupVersion != null
|
||||
val savedBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo()
|
||||
|
||||
val megolmKeyKnown = savedBackupKey?.version == currentBackupVersion
|
||||
SecretsSynchronisationInfo(
|
||||
isBackupSetup = is4SSetup,
|
||||
isCrossSigningEnabled = isCrossSigningEnabled,
|
||||
isCrossSigningTrusted = isCrossSigningTrusted,
|
||||
allPrivateKeysKnown = allPrivateKeysKnown,
|
||||
megolmBackupAvailable = megolmBackupAvailable,
|
||||
megolmSecretKnown = megolmKeyKnown,
|
||||
isMegolmKeyIn4S = session.sharedSecretStorageService.isMegolmKeyInBackup()
|
||||
)
|
||||
}
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun Session.rx(): RxSession {
|
||||
|
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
data class SecretsSynchronisationInfo(
|
||||
val isBackupSetup: Boolean,
|
||||
val isCrossSigningEnabled: Boolean,
|
||||
val isCrossSigningTrusted: Boolean,
|
||||
val allPrivateKeysKnown: Boolean,
|
||||
val megolmBackupAvailable: Boolean,
|
||||
val megolmSecretKnown: Boolean,
|
||||
val isMegolmKeyIn4S: Boolean
|
||||
)
|
@@ -23,7 +23,7 @@ android {
|
||||
testOptions.unitTests.includeAndroidResources = true
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "0.0.1"
|
||||
@@ -31,14 +31,26 @@ android {
|
||||
multiDexEnabled true
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// The following argument makes the Android Test Orchestrator run its
|
||||
// "pm clear" command after each test invocation. This command ensures
|
||||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
||||
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
|
||||
|
||||
defaultConfig {
|
||||
consumerProguardFiles 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
execution 'ANDROIDX_TEST_ORCHESTRATOR'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
debug {
|
||||
// Set to true to log privacy or sensible data, such as token
|
||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
|
||||
@@ -49,9 +61,6 @@ android {
|
||||
release {
|
||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
||||
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
|
||||
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +122,7 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||
implementation "androidx.core:core-ktx:1.3.0"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
@@ -158,6 +168,13 @@ dependencies {
|
||||
// Bus
|
||||
implementation 'org.greenrobot:eventbus:3.1.1'
|
||||
|
||||
// Phone number https://github.com/google/libphonenumber
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
||||
|
||||
// Web RTC
|
||||
// TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
|
||||
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
|
||||
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
|
||||
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
|
||||
androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0'
|
||||
@@ -172,6 +189,7 @@ dependencies {
|
||||
// Plant Timber tree for test
|
||||
testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
kaptAndroidTest "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
@@ -184,4 +202,6 @@ dependencies {
|
||||
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
// Plant Timber tree for test
|
||||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.2.0'
|
||||
}
|
||||
|
61
matrix-sdk-android/proguard-rules.pro
vendored
61
matrix-sdk-android/proguard-rules.pro
vendored
@@ -19,3 +19,64 @@
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
|
||||
### EVENT BUS ###
|
||||
|
||||
-keepattributes *Annotation*
|
||||
-keepclassmembers class * {
|
||||
@org.greenrobot.eventbus.Subscribe <methods>;
|
||||
}
|
||||
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
|
||||
|
||||
### MOSHI ###
|
||||
|
||||
# JSR 305 annotations are for embedding nullability information.
|
||||
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@com.squareup.moshi.* <methods>;
|
||||
}
|
||||
|
||||
-keep @com.squareup.moshi.JsonQualifier interface *
|
||||
|
||||
# Enum field names are used by the integrated EnumJsonAdapter.
|
||||
# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly
|
||||
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
|
||||
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
|
||||
<fields>;
|
||||
**[] values();
|
||||
}
|
||||
|
||||
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
|
||||
|
||||
-keepclassmembers class kotlin.Metadata {
|
||||
public <methods>;
|
||||
}
|
||||
|
||||
### OKHTTP for Android Studio ###
|
||||
-keep class okhttp3.Headers { *; }
|
||||
-keep interface okhttp3.Interceptor.* { *; }
|
||||
|
||||
### OLM JNI ###
|
||||
-keep class org.matrix.olm.** { *; }
|
||||
|
||||
### Webrtc
|
||||
-keep class org.webrtc.** { *; }
|
||||
|
||||
### Serializable persisted classes
|
||||
# https://www.guardsquare.com/en/products/proguard/manual/examples#serializable
|
||||
-keepnames class * implements java.io.Serializable
|
||||
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||
!static !transient <fields>;
|
||||
!private <fields>;
|
||||
!private <methods>;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.auth.AuthenticationService
|
||||
import im.vector.matrix.android.common.DaggerTestMatrixComponent
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||
import im.vector.matrix.android.internal.network.UserAgentHolder
|
||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||
import org.matrix.olm.OlmManager
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This is the main entry point to the matrix sdk.
|
||||
* To get the singleton instance, use getInstance static method.
|
||||
*/
|
||||
class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||
|
||||
@Inject internal lateinit var authenticationService: AuthenticationService
|
||||
@Inject internal lateinit var userAgentHolder: UserAgentHolder
|
||||
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
|
||||
@Inject internal lateinit var olmManager: OlmManager
|
||||
@Inject internal lateinit var sessionManager: SessionManager
|
||||
|
||||
init {
|
||||
Monarchy.init(context)
|
||||
DaggerTestMatrixComponent.factory().create(context, matrixConfiguration).inject(this)
|
||||
if (context.applicationContext !is Configuration.Provider) {
|
||||
WorkManager.initialize(context, Configuration.Builder().setExecutor(Executors.newCachedThreadPool()).build())
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
|
||||
}
|
||||
|
||||
fun getUserAgent() = userAgentHolder.userAgent
|
||||
|
||||
fun authenticationService(): AuthenticationService {
|
||||
return authenticationService
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private lateinit var instance: Matrix
|
||||
private val isInit = AtomicBoolean(false)
|
||||
|
||||
fun initialize(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||
if (isInit.compareAndSet(false, true)) {
|
||||
instance = Matrix(context.applicationContext, matrixConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
fun getInstance(context: Context): Matrix {
|
||||
if (isInit.compareAndSet(false, true)) {
|
||||
val appContext = context.applicationContext
|
||||
if (appContext is MatrixConfiguration.Provider) {
|
||||
val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration()
|
||||
instance = Matrix(appContext, matrixConfiguration)
|
||||
} else {
|
||||
throw IllegalStateException("Matrix is not initialized properly." +
|
||||
" You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
|
||||
}
|
||||
}
|
||||
return instance
|
||||
}
|
||||
|
||||
fun getSdkVersion(): String {
|
||||
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
|
||||
}
|
||||
|
||||
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
|
||||
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
|
||||
}
|
||||
}
|
||||
}
|
@@ -28,10 +28,10 @@ import im.vector.matrix.android.api.auth.data.LoginFlowResult
|
||||
import im.vector.matrix.android.api.auth.registration.RegistrationResult
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
@@ -57,9 +57,10 @@ class CommonTestHelper(context: Context) {
|
||||
|
||||
val matrix: Matrix
|
||||
|
||||
fun getTestInterceptor(session: Session): MockOkHttpInterceptor? = TestNetworkModule.interceptorForSession(session.sessionId) as? MockOkHttpInterceptor
|
||||
|
||||
init {
|
||||
Matrix.initialize(context, MatrixConfiguration("TestFlavor"))
|
||||
|
||||
matrix = Matrix.getInstance(context)
|
||||
}
|
||||
|
||||
@@ -88,7 +89,8 @@ class CommonTestHelper(context: Context) {
|
||||
fun syncSession(session: Session) {
|
||||
val lock = CountDownLatch(1)
|
||||
|
||||
session.open()
|
||||
GlobalScope.launch(Dispatchers.Main) { session.open() }
|
||||
|
||||
session.startSync(true)
|
||||
|
||||
val syncLiveData = runBlocking(Dispatchers.Main) {
|
||||
@@ -115,8 +117,9 @@ class CommonTestHelper(context: Context) {
|
||||
* @param nbOfMessages the number of time the message will be sent
|
||||
*/
|
||||
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> {
|
||||
val timeline = room.createTimeline(null, TimelineSettings(10))
|
||||
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
|
||||
val latch = CountDownLatch(nbOfMessages)
|
||||
val latch = CountDownLatch(1)
|
||||
val timelineListener = object : Timeline.Listener {
|
||||
override fun onTimelineFailure(throwable: Throwable) {
|
||||
}
|
||||
@@ -127,28 +130,29 @@ class CommonTestHelper(context: Context) {
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
val newMessages = snapshot
|
||||
.filter { LocalEcho.isLocalEchoId(it.eventId).not() }
|
||||
.filter { it.root.sendState == SendState.SYNCED }
|
||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||
.filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
|
||||
|
||||
if (newMessages.size == nbOfMessages) {
|
||||
sentEvents.addAll(newMessages)
|
||||
// Remove listener now, if not at the next update sendEvents could change
|
||||
timeline.removeListener(this)
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
val timeline = room.createTimeline(null, TimelineSettings(10))
|
||||
timeline.start()
|
||||
timeline.addListener(timelineListener)
|
||||
for (i in 0 until nbOfMessages) {
|
||||
room.sendTextMessage(message + " #" + (i + 1))
|
||||
}
|
||||
await(latch)
|
||||
timeline.removeListener(timelineListener)
|
||||
// Wait 3 second more per message
|
||||
await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages)
|
||||
timeline.dispose()
|
||||
|
||||
// Check that all events has been created
|
||||
assertEquals(nbOfMessages.toLong(), sentEvents.size.toLong())
|
||||
assertEquals("Message number do not match $sentEvents", nbOfMessages.toLong(), sentEvents.size.toLong())
|
||||
|
||||
return sentEvents
|
||||
}
|
||||
@@ -291,6 +295,24 @@ class CommonTestHelper(context: Context) {
|
||||
return requestFailure!!
|
||||
}
|
||||
|
||||
fun createEventListener(latch: CountDownLatch, predicate: (List<TimelineEvent>) -> Boolean): Timeline.Listener {
|
||||
return object : Timeline.Listener {
|
||||
override fun onTimelineFailure(throwable: Throwable) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||
// noop
|
||||
}
|
||||
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
if (predicate(snapshot)) {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Await for a latch and ensure the result is true
|
||||
*
|
||||
@@ -349,3 +371,13 @@ class CommonTestHelper(context: Context) {
|
||||
session.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun List<TimelineEvent>.checkSendOrder(baseTextMessage: String, numberOfMessages: Int, startIndex: Int): Boolean {
|
||||
return drop(startIndex)
|
||||
.take(numberOfMessages)
|
||||
.foldRightIndexed(true) { index, timelineEvent, acc ->
|
||||
val body = timelineEvent.root.content.toModel<MessageContent>()?.body
|
||||
val currentMessageSuffix = numberOfMessages - index
|
||||
acc && (body == null || body.startsWith(baseTextMessage) && body.endsWith("#$currentMessageSuffix"))
|
||||
}
|
||||
}
|
||||
|
@@ -17,8 +17,13 @@
|
||||
package im.vector.matrix.android.common
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Observer
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.verification.IncomingSasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.OutgoingSasVerificationTransaction
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
@@ -34,6 +39,7 @@ import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -41,6 +47,8 @@ import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
@@ -53,17 +61,19 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
/**
|
||||
* @return alice session
|
||||
*/
|
||||
fun doE2ETestWithAliceInARoom(): CryptoTestData {
|
||||
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData {
|
||||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
|
||||
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
|
||||
}
|
||||
|
||||
val room = aliceSession.getRoom(roomId)!!
|
||||
if (encryptedRoom) {
|
||||
val room = aliceSession.getRoom(roomId)!!
|
||||
|
||||
mTestHelper.doSync<Unit> {
|
||||
room.enableEncryption(callback = it)
|
||||
mTestHelper.doSync<Unit> {
|
||||
room.enableEncryption(callback = it)
|
||||
}
|
||||
}
|
||||
|
||||
return CryptoTestData(aliceSession, roomId)
|
||||
@@ -72,8 +82,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
/**
|
||||
* @return alice and bob sessions
|
||||
*/
|
||||
fun doE2ETestWithAliceAndBobInARoom(): CryptoTestData {
|
||||
val cryptoTestData = doE2ETestWithAliceInARoom()
|
||||
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData {
|
||||
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom)
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
@@ -165,7 +175,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
}
|
||||
|
||||
mTestHelper.doSync<Unit> {
|
||||
samSession.joinRoom(room.roomId, null, it)
|
||||
samSession.joinRoom(room.roomId, null, emptyList(), it)
|
||||
}
|
||||
|
||||
return samSession
|
||||
@@ -239,14 +249,14 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
val eventWireContent = event.content.toContent()
|
||||
assertNotNull(eventWireContent)
|
||||
|
||||
assertNull(eventWireContent.get("body"))
|
||||
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm"))
|
||||
assertNull(eventWireContent["body"])
|
||||
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
|
||||
|
||||
assertNotNull(eventWireContent.get("ciphertext"))
|
||||
assertNotNull(eventWireContent.get("session_id"))
|
||||
assertNotNull(eventWireContent.get("sender_key"))
|
||||
assertNotNull(eventWireContent["ciphertext"])
|
||||
assertNotNull(eventWireContent["session_id"])
|
||||
assertNotNull(eventWireContent["sender_key"])
|
||||
|
||||
assertEquals(senderSession.sessionParams.credentials.deviceId, eventWireContent.get("device_id"))
|
||||
assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
|
||||
|
||||
assertNotNull(event.eventId)
|
||||
assertEquals(roomId, event.roomId)
|
||||
@@ -255,7 +265,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
|
||||
val eventContent = event.toContent()
|
||||
assertNotNull(eventContent)
|
||||
assertEquals(clearMessage, eventContent.get("body"))
|
||||
assertEquals(clearMessage, eventContent["body"])
|
||||
assertEquals(senderSession.myUserId, event.senderId)
|
||||
}
|
||||
|
||||
@@ -272,4 +282,143 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
authData = createFakeMegolmBackupAuthData()
|
||||
)
|
||||
}
|
||||
|
||||
fun createDM(alice: Session, bob: Session): String {
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
alice.createRoom(
|
||||
CreateRoomParams().apply {
|
||||
invitedUserIds.add(bob.myUserId)
|
||||
setDirectMessage()
|
||||
enableEncryptionIfInvitedUsersSupportIt = true
|
||||
},
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
|
||||
bob.getRoomSummariesLive(roomSummaryQueryParams { })
|
||||
}
|
||||
|
||||
val newRoomObserver = object : Observer<List<RoomSummary>> {
|
||||
override fun onChanged(t: List<RoomSummary>?) {
|
||||
val indexOfFirst = t?.indexOfFirst { it.roomId == roomId } ?: -1
|
||||
if (indexOfFirst != -1) {
|
||||
latch.countDown()
|
||||
bobRoomSummariesLive.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
bobRoomSummariesLive.observeForever(newRoomObserver)
|
||||
}
|
||||
}
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
val bobRoomSummariesLive = runBlocking(Dispatchers.Main) {
|
||||
bob.getRoomSummariesLive(roomSummaryQueryParams { })
|
||||
}
|
||||
|
||||
val newRoomObserver = object : Observer<List<RoomSummary>> {
|
||||
override fun onChanged(t: List<RoomSummary>?) {
|
||||
if (bob.getRoom(roomId)
|
||||
?.getRoomMember(bob.myUserId)
|
||||
?.membership == Membership.JOIN) {
|
||||
latch.countDown()
|
||||
bobRoomSummariesLive.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
bobRoomSummariesLive.observeForever(newRoomObserver)
|
||||
}
|
||||
|
||||
mTestHelper.doSync<Unit> { bob.joinRoom(roomId, callback = it) }
|
||||
}
|
||||
|
||||
return roomId
|
||||
}
|
||||
|
||||
fun initializeCrossSigning(session: Session) {
|
||||
mTestHelper.doSync<Unit> {
|
||||
session.cryptoService().crossSigningService()
|
||||
.initializeCrossSigning(UserPasswordAuth(
|
||||
user = session.myUserId,
|
||||
password = TestConstants.PASSWORD
|
||||
), it)
|
||||
}
|
||||
}
|
||||
|
||||
fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
|
||||
assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
|
||||
assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
|
||||
|
||||
val requestID = UUID.randomUUID().toString()
|
||||
val aliceVerificationService = alice.cryptoService().verificationService()
|
||||
val bobVerificationService = bob.cryptoService().verificationService()
|
||||
|
||||
aliceVerificationService.beginKeyVerificationInDMs(
|
||||
VerificationMethod.SAS,
|
||||
requestID,
|
||||
roomId,
|
||||
bob.myUserId,
|
||||
bob.sessionParams.credentials.deviceId!!,
|
||||
null)
|
||||
|
||||
// we should reach SHOW SAS on both
|
||||
var alicePovTx: OutgoingSasVerificationTransaction? = null
|
||||
var bobPovTx: IncomingSasVerificationTransaction? = null
|
||||
|
||||
// wait for alice to get the ready
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
||||
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||
bobPovTx?.performAccept()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID) as? OutgoingSasVerificationTransaction
|
||||
Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
|
||||
alicePovTx?.state == VerificationTxState.ShortCodeReady
|
||||
}
|
||||
}
|
||||
// wait for alice to get the ready
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID) as? IncomingSasVerificationTransaction
|
||||
Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
|
||||
if (bobPovTx?.state == VerificationTxState.OnStarted) {
|
||||
bobPovTx?.performAccept()
|
||||
}
|
||||
bobPovTx?.state == VerificationTxState.ShortCodeReady
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation())
|
||||
|
||||
bobPovTx!!.userHasVerifiedShortCode()
|
||||
alicePovTx!!.userHasVerifiedShortCode()
|
||||
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
|
||||
}
|
||||
}
|
||||
|
||||
mTestHelper.waitWithLatch {
|
||||
mTestHelper.retryPeriodicallyWithLatch(it) {
|
||||
alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package im.vector.matrix.android.common
|
||||
|
||||
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
@@ -37,7 +38,7 @@ import javax.net.ssl.HttpsURLConnection
|
||||
* AutoDiscovery().findClientConfig("matrix.org", <callback>)
|
||||
* </code>
|
||||
*/
|
||||
class MockOkHttpInterceptor : Interceptor {
|
||||
class MockOkHttpInterceptor : TestInterceptor {
|
||||
|
||||
private var rules: ArrayList<Rule> = ArrayList()
|
||||
|
||||
@@ -45,6 +46,12 @@ class MockOkHttpInterceptor : Interceptor {
|
||||
rules.add(rule)
|
||||
}
|
||||
|
||||
fun clearRules() {
|
||||
rules.clear()
|
||||
}
|
||||
|
||||
override var sessionId: String? = null
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
|
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.common
|
||||
|
||||
import android.content.Context
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import im.vector.matrix.android.api.MatrixConfiguration
|
||||
import im.vector.matrix.android.internal.auth.AuthModule
|
||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||
import im.vector.matrix.android.internal.di.MatrixModule
|
||||
import im.vector.matrix.android.internal.di.MatrixScope
|
||||
import im.vector.matrix.android.internal.di.NetworkModule
|
||||
|
||||
@Component(modules = [TestModule::class, MatrixModule::class, NetworkModule::class, AuthModule::class, TestNetworkModule::class])
|
||||
@MatrixScope
|
||||
internal interface TestMatrixComponent : MatrixComponent {
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(@BindsInstance context: Context,
|
||||
@BindsInstance matrixConfiguration: MatrixConfiguration): TestMatrixComponent
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.common
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import im.vector.matrix.android.internal.di.MatrixComponent
|
||||
|
||||
@Module
|
||||
internal abstract class TestModule {
|
||||
@Binds
|
||||
abstract fun providesMatrixComponent(testMatrixComponent: TestMatrixComponent): MatrixComponent
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.common
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import im.vector.matrix.android.internal.session.MockHttpInterceptor
|
||||
import im.vector.matrix.android.internal.session.TestInterceptor
|
||||
|
||||
@Module
|
||||
internal object TestNetworkModule {
|
||||
|
||||
val interceptors = ArrayList<TestInterceptor>()
|
||||
|
||||
fun interceptorForSession(sessionId: String): TestInterceptor? = interceptors.firstOrNull { it.sessionId == sessionId }
|
||||
|
||||
@Provides
|
||||
@JvmStatic
|
||||
@MockHttpInterceptor
|
||||
fun providesTestInterceptor(): TestInterceptor? {
|
||||
return MockOkHttpInterceptor().also {
|
||||
interceptors.add(it)
|
||||
}
|
||||
}
|
||||
}
|
@@ -22,10 +22,8 @@ import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import io.realm.Realm
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -45,22 +43,22 @@ class CryptoStoreTest : InstrumentedTest {
|
||||
Realm.init(context())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_metadata_realm_ok() {
|
||||
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
||||
|
||||
assertFalse(cryptoStore.hasData())
|
||||
|
||||
cryptoStore.open()
|
||||
|
||||
assertEquals("deviceId_sample", cryptoStore.getDeviceId())
|
||||
|
||||
assertTrue(cryptoStore.hasData())
|
||||
|
||||
// Cleanup
|
||||
cryptoStore.close()
|
||||
cryptoStore.deleteStore()
|
||||
}
|
||||
// @Test
|
||||
// fun test_metadata_realm_ok() {
|
||||
// val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
||||
//
|
||||
// assertFalse(cryptoStore.hasData())
|
||||
//
|
||||
// cryptoStore.open()
|
||||
//
|
||||
// assertEquals("deviceId_sample", cryptoStore.getDeviceId())
|
||||
//
|
||||
// assertTrue(cryptoStore.hasData())
|
||||
//
|
||||
// // Cleanup
|
||||
// cryptoStore.close()
|
||||
// cryptoStore.deleteStore()
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun test_lastSessionUsed() {
|
||||
|
@@ -122,7 +122,7 @@ class XSigningTest : InstrumentedTest {
|
||||
// We will want to test that in alice POV, this new device would be trusted by cross signing
|
||||
|
||||
val bobSession2 = mTestHelper.logIntoAccount(bobUserId, SessionTestParams(true))
|
||||
val bobSecondDeviceId = bobSession2.sessionParams.credentials.deviceId!!
|
||||
val bobSecondDeviceId = bobSession2.sessionParams.deviceId!!
|
||||
|
||||
// Check that bob first session sees the new login
|
||||
val data = mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
|
||||
|
@@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.SessionTestParams
|
||||
import im.vector.matrix.android.common.TestConstants
|
||||
import im.vector.matrix.android.internal.crypto.GossipingRequestState
|
||||
@@ -56,6 +57,7 @@ import java.util.concurrent.CountDownLatch
|
||||
class KeyShareTests : InstrumentedTest {
|
||||
|
||||
private val mTestHelper = CommonTestHelper(context())
|
||||
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
|
||||
|
||||
@Test
|
||||
fun test_DoNotSelfShareIfNotTrusted() {
|
||||
@@ -64,7 +66,10 @@ class KeyShareTests : InstrumentedTest {
|
||||
// Create an encrypted room and add a message
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
aliceSession.createRoom(
|
||||
CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
|
||||
CreateRoomParams().apply {
|
||||
visibility = RoomDirectoryVisibility.PRIVATE
|
||||
enableEncryption()
|
||||
},
|
||||
it
|
||||
)
|
||||
}
|
||||
@@ -90,7 +95,7 @@ class KeyShareTests : InstrumentedTest {
|
||||
} catch (failure: Throwable) {
|
||||
}
|
||||
|
||||
val outgoingRequestBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
// Try to request
|
||||
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
|
||||
|
||||
@@ -100,10 +105,10 @@ class KeyShareTests : InstrumentedTest {
|
||||
var outGoingRequestId: String? = null
|
||||
|
||||
mTestHelper.retryPeriodicallyWithLatch(waitLatch) {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
.filter { req ->
|
||||
// filter out request that was known before
|
||||
!outgoingRequestBefore.any { req.requestId == it.requestId }
|
||||
!outgoingRequestsBefore.any { req.requestId == it.requestId }
|
||||
}
|
||||
.let {
|
||||
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
|
||||
@@ -115,10 +120,10 @@ class KeyShareTests : InstrumentedTest {
|
||||
|
||||
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
|
||||
|
||||
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
|
||||
// We should have a new request
|
||||
Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestBefore.size)
|
||||
Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestsBefore.size)
|
||||
Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId })
|
||||
|
||||
// The first session should see an incoming request
|
||||
@@ -126,7 +131,7 @@ class KeyShareTests : InstrumentedTest {
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
// DEBUG LOGS
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
||||
Log.v("TEST", "=========================")
|
||||
it.forEach { keyRequest ->
|
||||
@@ -135,7 +140,7 @@ class KeyShareTests : InstrumentedTest {
|
||||
Log.v("TEST", "=========================")
|
||||
}
|
||||
|
||||
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequest().firstOrNull { it.requestId == outGoingRequestId }
|
||||
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
|
||||
incoming?.state == GossipingRequestState.REJECTED
|
||||
}
|
||||
}
|
||||
@@ -148,14 +153,14 @@ class KeyShareTests : InstrumentedTest {
|
||||
|
||||
// Mark the device as trusted
|
||||
aliceSession.cryptoService().setDeviceVerification(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true), aliceSession.myUserId,
|
||||
aliceSession2.sessionParams.credentials.deviceId ?: "")
|
||||
aliceSession2.sessionParams.deviceId ?: "")
|
||||
|
||||
// Re request
|
||||
aliceSession2.cryptoService().reRequestRoomKeyForEvent(receivedEvent.root)
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
Log.v("TEST", "Incoming request Session 1")
|
||||
Log.v("TEST", "=========================")
|
||||
it.forEach {
|
||||
@@ -171,7 +176,7 @@ class KeyShareTests : InstrumentedTest {
|
||||
Thread.sleep(6_000)
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequest().let {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
|
||||
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
|
||||
}
|
||||
}
|
||||
@@ -234,6 +239,7 @@ class KeyShareTests : InstrumentedTest {
|
||||
}
|
||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||
session1ShortCode = tx.getDecimalCodeRepresentation()
|
||||
Thread.sleep(500)
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
@@ -246,19 +252,20 @@ class KeyShareTests : InstrumentedTest {
|
||||
if (tx is SasVerificationTransaction) {
|
||||
if (tx.state == VerificationTxState.ShortCodeReady) {
|
||||
session2ShortCode = tx.getDecimalCodeRepresentation()
|
||||
Thread.sleep(500)
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val txId: String = "m.testVerif12"
|
||||
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.credentials.deviceId
|
||||
val txId = "m.testVerif12"
|
||||
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
|
||||
?: "", txId)
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.credentials.deviceId ?: "")?.isVerified == true
|
||||
aliceSession1.cryptoService().getDeviceInfo(aliceSession1.myUserId, aliceSession2.sessionParams.deviceId ?: "")?.isVerified == true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,9 +288,12 @@ class KeyShareTests : InstrumentedTest {
|
||||
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||
Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||
}
|
||||
}
|
||||
|
||||
mTestHelper.signOutAndClose(aliceSession1)
|
||||
mTestHelper.signOutAndClose(aliceSession2)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.gossiping
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.MockOkHttpInterceptor
|
||||
import im.vector.matrix.android.common.SessionTestParams
|
||||
import im.vector.matrix.android.common.TestConstants
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.WithHeldCode
|
||||
import org.junit.Assert
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class WithHeldTests : InstrumentedTest {
|
||||
|
||||
private val mTestHelper = CommonTestHelper(context())
|
||||
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
|
||||
|
||||
@Test
|
||||
fun test_WithHeldUnverifiedReason() {
|
||||
// =============================
|
||||
// ARRANGE
|
||||
// =============================
|
||||
|
||||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
val bobSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
|
||||
|
||||
// Initialize cross signing on both
|
||||
mCryptoTestHelper.initializeCrossSigning(aliceSession)
|
||||
mCryptoTestHelper.initializeCrossSigning(bobSession)
|
||||
|
||||
val roomId = mCryptoTestHelper.createDM(aliceSession, bobSession)
|
||||
mCryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, roomId)
|
||||
|
||||
val roomAlicePOV = aliceSession.getRoom(roomId)!!
|
||||
|
||||
val bobUnverifiedSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||
|
||||
// =============================
|
||||
// ACT
|
||||
// =============================
|
||||
|
||||
// Alice decide to not send to unverified sessions
|
||||
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
|
||||
|
||||
val timelineEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first()
|
||||
|
||||
// await for bob unverified session to get the message
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId) != null
|
||||
}
|
||||
}
|
||||
|
||||
val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(timelineEvent.eventId)!!
|
||||
|
||||
// =============================
|
||||
// ASSERT
|
||||
// =============================
|
||||
|
||||
// Bob should not be able to decrypt because the keys is withheld
|
||||
try {
|
||||
// .. might need to wait a bit for stability?
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||
Assert.fail("This session should not be able to decrypt")
|
||||
} catch (failure: Throwable) {
|
||||
val type = (failure as MXCryptoError.Base).errorType
|
||||
val technicalMessage = failure.technicalMessage
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
||||
}
|
||||
|
||||
// enable back sending to unverified
|
||||
aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
|
||||
|
||||
val secondEvent = mTestHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first()
|
||||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val ev = bobUnverifiedSession.getRoom(roomId)?.getTimeLineEvent(secondEvent.eventId)
|
||||
// wait until it's decrypted
|
||||
ev?.root?.getClearType() == EventType.MESSAGE
|
||||
}
|
||||
}
|
||||
|
||||
// Previous message should still be undecryptable (partially withheld session)
|
||||
try {
|
||||
// .. might need to wait a bit for stability?
|
||||
bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
|
||||
Assert.fail("This session should not be able to decrypt")
|
||||
} catch (failure: Throwable) {
|
||||
val type = (failure as MXCryptoError.Base).errorType
|
||||
val technicalMessage = failure.technicalMessage
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
|
||||
}
|
||||
|
||||
mTestHelper.signOutAndClose(aliceSession)
|
||||
mTestHelper.signOutAndClose(bobSession)
|
||||
mTestHelper.signOutAndClose(bobUnverifiedSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_WithHeldNoOlm() {
|
||||
val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
val aliceInterceptor = mTestHelper.getTestInterceptor(aliceSession)
|
||||
|
||||
// Simulate no OTK
|
||||
aliceInterceptor!!.addRule(MockOkHttpInterceptor.SimpleRule(
|
||||
"/keys/claim",
|
||||
200,
|
||||
"""
|
||||
{ "one_time_keys" : {} }
|
||||
"""
|
||||
))
|
||||
Log.d("#TEST", "Recovery :${aliceSession.sessionParams.credentials.accessToken}")
|
||||
|
||||
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
|
||||
|
||||
val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
|
||||
|
||||
// await for bob session to get the message
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId) != null
|
||||
}
|
||||
}
|
||||
|
||||
// Previous message should still be undecryptable (partially withheld session)
|
||||
val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)
|
||||
try {
|
||||
// .. might need to wait a bit for stability?
|
||||
bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
|
||||
Assert.fail("This session should not be able to decrypt")
|
||||
} catch (failure: Throwable) {
|
||||
val type = (failure as MXCryptoError.Base).errorType
|
||||
val technicalMessage = failure.technicalMessage
|
||||
Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
|
||||
Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage)
|
||||
}
|
||||
|
||||
// Ensure that alice has marked the session to be shared with bob
|
||||
val sessionId = eventBobPOV!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
|
||||
val chainIndex = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSession.myUserId, bobSession.sessionParams.credentials.deviceId)
|
||||
|
||||
Assert.assertEquals("Alice should have marked bob's device for this session", 0, chainIndex)
|
||||
// Add a new device for bob
|
||||
|
||||
aliceInterceptor.clearRules()
|
||||
val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(withInitialSync = true))
|
||||
// send a second message
|
||||
val secondMessageId = mTestHelper.sendTextMessage(roomAlicePov, "second message", 1).first().eventId
|
||||
|
||||
// Check that the
|
||||
// await for bob SecondSession session to get the message
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(secondMessageId) != null
|
||||
}
|
||||
}
|
||||
|
||||
val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(bobSecondSession.myUserId, bobSecondSession.sessionParams.credentials.deviceId)
|
||||
|
||||
Assert.assertEquals("Alice should have marked bob's device for this session", 1, chainIndex2)
|
||||
|
||||
aliceInterceptor.clearRules()
|
||||
testData.cleanUp(mTestHelper)
|
||||
mTestHelper.signOutAndClose(bobSecondSession)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_WithHeldKeyRequest() {
|
||||
val testData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
|
||||
val aliceSession = testData.firstSession
|
||||
val bobSession = testData.secondSession!!
|
||||
|
||||
val roomAlicePov = aliceSession.getRoom(testData.roomId)!!
|
||||
|
||||
val eventId = mTestHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
|
||||
|
||||
mTestHelper.signOutAndClose(bobSession)
|
||||
|
||||
// Create a new session for bob
|
||||
|
||||
val bobSecondSession = mTestHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
|
||||
// initialize to force request keys if missing
|
||||
mCryptoTestHelper.initializeCrossSigning(bobSecondSession)
|
||||
|
||||
// Trust bob second device from Alice POV
|
||||
aliceSession.cryptoService().crossSigningService().trustDevice(bobSecondSession.sessionParams.deviceId!!, NoOpMatrixCallback())
|
||||
bobSecondSession.cryptoService().crossSigningService().trustDevice(aliceSession.sessionParams.deviceId!!, NoOpMatrixCallback())
|
||||
|
||||
var sessionId: String? = null
|
||||
// Check that the
|
||||
// await for bob SecondSession session to get the message
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also {
|
||||
// try to decrypt and force key request
|
||||
tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
|
||||
}
|
||||
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
|
||||
timeLineEvent != null
|
||||
}
|
||||
}
|
||||
|
||||
// Check that bob second session requested the key
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
|
||||
wc?.code == WithHeldCode.UNAUTHORISED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -835,7 +835,7 @@ class KeysBackupTest : InstrumentedTest {
|
||||
assertTrue(signature.valid)
|
||||
assertNotNull(signature.device)
|
||||
assertEquals(cryptoTestData.firstSession.cryptoService().getMyDevice().deviceId, signature.deviceId)
|
||||
assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.credentials.deviceId)
|
||||
assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.sessionParams.deviceId)
|
||||
|
||||
stateObserver.stopAndCheckStates(null)
|
||||
cryptoTestData.cleanUp(mTestHelper)
|
||||
@@ -997,7 +997,7 @@ class KeysBackupTest : InstrumentedTest {
|
||||
keysBackup.backupAllGroupSessions(null, it)
|
||||
}
|
||||
|
||||
val oldDeviceId = cryptoTestData.firstSession.sessionParams.credentials.deviceId!!
|
||||
val oldDeviceId = cryptoTestData.firstSession.sessionParams.deviceId!!
|
||||
val oldKeyBackupVersion = keysBackup.currentBackupVersion
|
||||
val aliceUserId = cryptoTestData.firstSession.myUserId
|
||||
|
||||
|
@@ -144,7 +144,7 @@ class QuadSTests : InstrumentedTest {
|
||||
|
||||
val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
|
||||
|
||||
val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*, *>
|
||||
val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
|
||||
assertNotNull("Element should be encrypted", encryptedContent)
|
||||
assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId))
|
||||
|
||||
|
@@ -468,14 +468,19 @@ class SASTest : InstrumentedTest {
|
||||
|
||||
val aliceSASLatch = CountDownLatch(1)
|
||||
val aliceListener = object : VerificationService.Listener {
|
||||
var matchOnce = true
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
val uxState = (tx as OutgoingSasVerificationTransaction).uxState
|
||||
Log.v("TEST", "== aliceState ${uxState.name}")
|
||||
when (uxState) {
|
||||
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
|
||||
aliceSASLatch.countDown()
|
||||
if (matchOnce) {
|
||||
matchOnce = false
|
||||
aliceSASLatch.countDown()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
@@ -485,14 +490,23 @@ class SASTest : InstrumentedTest {
|
||||
|
||||
val bobSASLatch = CountDownLatch(1)
|
||||
val bobListener = object : VerificationService.Listener {
|
||||
var acceptOnce = true
|
||||
var matchOnce = true
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
val uxState = (tx as IncomingSasVerificationTransaction).uxState
|
||||
Log.v("TEST", "== bobState ${uxState.name}")
|
||||
when (uxState) {
|
||||
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
|
||||
tx.performAccept()
|
||||
if (acceptOnce) {
|
||||
acceptOnce = false
|
||||
tx.performAccept()
|
||||
}
|
||||
}
|
||||
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
|
||||
tx.userHasVerifiedShortCode()
|
||||
if (matchOnce) {
|
||||
matchOnce = false
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
|
||||
bobSASLatch.countDown()
|
||||
@@ -579,7 +593,7 @@ class SASTest : InstrumentedTest {
|
||||
requestID!!,
|
||||
cryptoTestData.roomId,
|
||||
bobSession.myUserId,
|
||||
bobSession.sessionParams.credentials.deviceId!!,
|
||||
bobSession.sessionParams.deviceId!!,
|
||||
null)
|
||||
|
||||
bobVerificationService.beginKeyVerificationInDMs(
|
||||
@@ -587,7 +601,7 @@ class SASTest : InstrumentedTest {
|
||||
requestID!!,
|
||||
cryptoTestData.roomId,
|
||||
aliceSession.myUserId,
|
||||
aliceSession.sessionParams.credentials.deviceId!!,
|
||||
aliceSession.sessionParams.deviceId!!,
|
||||
null)
|
||||
|
||||
// we should reach SHOW SAS on both
|
||||
|
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.send
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import org.commonmark.renderer.text.TextContentRenderer
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
||||
/**
|
||||
* It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild,
|
||||
* we can add more tests to cover the edge cases.
|
||||
* Some tests are suffixed with `_not_passing`, maybe one day we will fix them...
|
||||
* Riot-Web should be used as a reference for expected results, but not always. Especially Riot-Web add lots of `\n` in the
|
||||
* formatted body, which is quite useless.
|
||||
* Also Riot-Web does not provide plain text body when formatted text is provided. The body contains what the user has entered.
|
||||
* See https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection")
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class MarkdownParserTest : InstrumentedTest {
|
||||
|
||||
/**
|
||||
* Create the same parser than in the RoomModule
|
||||
*/
|
||||
private val markdownParser = MarkdownParser(
|
||||
Parser.builder().build(),
|
||||
HtmlRenderer.builder().build(),
|
||||
TextContentRenderer.builder().build()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun parseNoMarkdown() {
|
||||
testIdentity("")
|
||||
testIdentity("a")
|
||||
testIdentity("1")
|
||||
testIdentity("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et " +
|
||||
"dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea com" +
|
||||
"modo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pari" +
|
||||
"atur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseSpaces() {
|
||||
testIdentity(" ")
|
||||
testIdentity(" ")
|
||||
testIdentity("\n")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseNewLines() {
|
||||
testIdentity("line1\nline2")
|
||||
testIdentity("line1\nline2\nline3")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseBold() {
|
||||
testType(
|
||||
name = "bold",
|
||||
markdownPattern = "**",
|
||||
htmlExpectedTag = "strong"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseItalic() {
|
||||
testType(
|
||||
name = "italic",
|
||||
markdownPattern = "*",
|
||||
htmlExpectedTag = "em"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseItalic2() {
|
||||
// Riot-Web format
|
||||
"_italic_".let { markdownParser.parse(it) }.expect("italic", "<em>italic</em>")
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: the test is not passing, it does not work on Riot-Web neither
|
||||
*/
|
||||
@Test
|
||||
fun parseStrike_not_passing() {
|
||||
testType(
|
||||
name = "strike",
|
||||
markdownPattern = "~~",
|
||||
htmlExpectedTag = "del"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCode() {
|
||||
testType(
|
||||
name = "code",
|
||||
markdownPattern = "`",
|
||||
htmlExpectedTag = "code",
|
||||
plainTextPrefix = "\"",
|
||||
plainTextSuffix = "\""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCode2() {
|
||||
testType(
|
||||
name = "code",
|
||||
markdownPattern = "``",
|
||||
htmlExpectedTag = "code",
|
||||
plainTextPrefix = "\"",
|
||||
plainTextSuffix = "\""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseCode3() {
|
||||
testType(
|
||||
name = "code",
|
||||
markdownPattern = "```",
|
||||
htmlExpectedTag = "code",
|
||||
plainTextPrefix = "\"",
|
||||
plainTextSuffix = "\""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseUnorderedList() {
|
||||
"- item1".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li></ul>") }
|
||||
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li><li>item2</li></ul>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseOrderedList() {
|
||||
"1. item1".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li></ol>") }
|
||||
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li><li>item2</li></ol>") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseHorizontalLine() {
|
||||
"---".let { markdownParser.parse(it) }.expect("***", "<hr />")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseH2AndContent() {
|
||||
"a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "<h2>a</h2><p>b</p>")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQuote() {
|
||||
"> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "<blockquote><p>quoted</p></blockquote>")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseQuote_not_passing() {
|
||||
"> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "<blockquote><p>quoted<br/>line2</p></blockquote>")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseBoldItalic() {
|
||||
"*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "<em>italic</em> <strong>bold</strong>")
|
||||
"**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "<strong>bold</strong> <em>italic</em>")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseHead() {
|
||||
"# head1".let { markdownParser.parse(it) }.expect("head1", "<h1>head1</h1>")
|
||||
"## head2".let { markdownParser.parse(it) }.expect("head2", "<h2>head2</h2>")
|
||||
"### head3".let { markdownParser.parse(it) }.expect("head3", "<h3>head3</h3>")
|
||||
"#### head4".let { markdownParser.parse(it) }.expect("head4", "<h4>head4</h4>")
|
||||
"##### head5".let { markdownParser.parse(it) }.expect("head5", "<h5>head5</h5>")
|
||||
"###### head6".let { markdownParser.parse(it) }.expect("head6", "<h6>head6</h6>")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseHeads() {
|
||||
"# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "<h1>head1</h1><h1>head2</h1>")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseBoldNewLines_not_passing() {
|
||||
"**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "<strong>bold</strong><br />line2")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseLinks() {
|
||||
"[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """<a href="target">link</a>""")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parseParagraph() {
|
||||
"# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "<h1>head</h1><p>content</p>")
|
||||
}
|
||||
|
||||
private fun testIdentity(text: String) {
|
||||
markdownParser.parse(text).expect(text, null)
|
||||
}
|
||||
|
||||
private fun testType(name: String,
|
||||
markdownPattern: String,
|
||||
htmlExpectedTag: String,
|
||||
plainTextPrefix: String = "",
|
||||
plainTextSuffix: String = "") {
|
||||
// Test simple case
|
||||
"$markdownPattern$name$markdownPattern"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix",
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
|
||||
// Test twice the same tag
|
||||
"$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix and $plainTextPrefix$name bis$plainTextSuffix",
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
|
||||
|
||||
val textBefore = "a"
|
||||
val textAfter = "b"
|
||||
|
||||
// With sticked text before
|
||||
"$textBefore$markdownPattern$name$markdownPattern"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix",
|
||||
expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
|
||||
// With text before and space
|
||||
"$textBefore $markdownPattern$name$markdownPattern"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix",
|
||||
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
|
||||
|
||||
// With sticked text after
|
||||
"$markdownPattern$name$markdownPattern$textAfter"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter",
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
|
||||
|
||||
// With space and text after
|
||||
"$markdownPattern$name$markdownPattern $textAfter"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter",
|
||||
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
|
||||
|
||||
// With sticked text before and text after
|
||||
"$textBefore$markdownPattern$name$markdownPattern$textAfter"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter",
|
||||
expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
|
||||
|
||||
// With text before and after, with spaces
|
||||
"$textBefore $markdownPattern$name$markdownPattern $textAfter"
|
||||
.let { markdownParser.parse(it) }
|
||||
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter",
|
||||
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
|
||||
}
|
||||
|
||||
private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) {
|
||||
assertEquals("TextContent are not identical", TextContent(expectedText, expectedFormattedText), this)
|
||||
}
|
||||
}
|
@@ -59,7 +59,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
|
||||
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
|
||||
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
|
||||
realm.copyToRealmOrUpdate(it)
|
||||
}
|
||||
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||
@@ -71,7 +71,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
fun add_shouldNotAdd_whenAlreadyIncluded() {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED).let {
|
||||
val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
|
||||
realm.copyToRealmOrUpdate(it)
|
||||
}
|
||||
chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
|
||||
@@ -141,7 +141,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
events: List<Event>,
|
||||
direction: PaginationDirection) {
|
||||
events.forEach { event ->
|
||||
val fakeEvent = event.toEntity(roomId, SendState.SYNCED).let {
|
||||
val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let {
|
||||
realm.copyToRealmOrUpdate(it)
|
||||
}
|
||||
addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
|
||||
|
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.checkSendOrder
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class TimelineBackToPreviousLastForwardTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
/**
|
||||
* This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an
|
||||
* even contained in a previous lastForward chunk, we will be able to go back to the live
|
||||
*/
|
||||
@Test
|
||||
fun backToPreviousLastForwardTest() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
|
||||
bobTimeline.start()
|
||||
|
||||
var roomCreationEventId: String? = null
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
roomCreationEventId = snapshot.lastOrNull()?.root?.eventId
|
||||
// Ok, we have the 8 first messages of the initial sync (room creation and bob join event)
|
||||
snapshot.size == 8
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob stop to sync
|
||||
bobSession.stopSync()
|
||||
|
||||
val messageRoot = "First messages from Alice"
|
||||
|
||||
// Alice sends 30 messages
|
||||
commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
messageRoot,
|
||||
30)
|
||||
|
||||
// Bob start to sync
|
||||
bobSession.startSync(true)
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 10 last messages from Alice.
|
||||
snapshot.size == 10
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(messageRoot).orFalse() }
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob navigate to the first event (room creation event), so inside the previous last forward chunk
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?)
|
||||
snapshot.size == 4
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically
|
||||
assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null)
|
||||
|
||||
bobTimeline.restartWithEventId(roomCreationEventId)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob scroll to the future
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Bob can see the first event of the room (so Back pagination has worked)
|
||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
// 8 for room creation item, and 30 for the forward pagination
|
||||
&& snapshot.size == 38
|
||||
&& snapshot.checkSendOrder(messageRoot, 30, 0)
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
bobTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.checkSendOrder
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class TimelineForwardPaginationTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
/**
|
||||
* This test ensure that if we click to permalink, we will be able to go back to the live
|
||||
*/
|
||||
@Test
|
||||
fun forwardPaginationTest() {
|
||||
val numberOfMessagesToSend = 90
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
|
||||
// Alice sends X messages
|
||||
val message = "Message from Alice"
|
||||
val sentMessages = commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
message,
|
||||
numberOfMessagesToSend)
|
||||
|
||||
// Alice clear the cache
|
||||
commonTestHelper.doSync<Unit> {
|
||||
aliceSession.clearCache(it)
|
||||
}
|
||||
|
||||
// And restarts the sync
|
||||
aliceSession.startSync(true)
|
||||
|
||||
val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30))
|
||||
aliceTimeline.start()
|
||||
|
||||
// Alice sees the 10 last message of the room, and can only navigate BACKWARD
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
|
||||
// Ok, we have the 10 last messages of the initial sync
|
||||
snapshot.size == 10
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(message).orFalse() }
|
||||
}
|
||||
|
||||
// Open the timeline at last sent message
|
||||
aliceTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Alice navigates to the first message of the room, which is not in its database. A GET /context is performed
|
||||
// Then she can paginate BACKWARD and FORWARD
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
|
||||
// The event is not in db, so it is fetch alone
|
||||
snapshot.size == 1
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith("Message from Alice").orFalse() }
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event
|
||||
aliceTimeline.restartWithEventId(sentMessages.last().eventId)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
}
|
||||
|
||||
// Alice paginates BACKWARD and FORWARD of 50 events each
|
||||
// Then she can only navigate FORWARD
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
|
||||
// Alice can see the first event of the room (so Back pagination has worked)
|
||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
|
||||
&& snapshot.size == 6 + 1 + 50
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event
|
||||
// We ask to load event backward and forward
|
||||
aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Alice paginates once again FORWARD for 50 events
|
||||
// All the timeline is retrieved, she cannot paginate anymore in both direction
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
|
||||
snapshot.size == 6 + numberOfMessagesToSend
|
||||
&& snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
// Ask for a forward pagination
|
||||
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
// The timeline is fully loaded
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
aliceTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.checkSendOrder
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class TimelinePreviousLastForwardTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
/**
|
||||
* This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink, we will be able to go back to the live
|
||||
*/
|
||||
@Test
|
||||
fun previousLastForwardTest() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
|
||||
bobTimeline.start()
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events)
|
||||
snapshot.size == 8
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob stop to sync
|
||||
bobSession.stopSync()
|
||||
|
||||
val firstMessage = "First messages from Alice"
|
||||
// Alice sends 30 messages
|
||||
val firstMessageFromAliceId = commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
firstMessage,
|
||||
30)
|
||||
.last()
|
||||
.eventId
|
||||
|
||||
// Bob start to sync
|
||||
bobSession.startSync(true)
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk
|
||||
snapshot.size == 10
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(firstMessage).orFalse() }
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob stop to sync
|
||||
bobSession.stopSync()
|
||||
|
||||
val secondMessage = "Second messages from Alice"
|
||||
// Alice sends again 30 messages
|
||||
commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
secondMessage,
|
||||
30)
|
||||
|
||||
// Bob start to sync
|
||||
bobSession.startSync(true)
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk
|
||||
snapshot.size == 10
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(secondMessage).orFalse() }
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob navigate to the first message sent from Alice
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// The event is not in db, so it is fetch
|
||||
snapshot.size == 1
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event, and paginate in both direction
|
||||
bobTimeline.restartWithEventId(firstMessageFromAliceId)
|
||||
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
}
|
||||
|
||||
// Paginate in both direction
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
snapshot.size == 8 + 1 + 35
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
// Paginate in both direction
|
||||
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||
// Ensure the chunk in the middle is included in the next pagination
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 35)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob scroll to the future, till the live
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Bob can see the first event of the room (so Back pagination has worked)
|
||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
// 8 for room creation item 60 message from Alice
|
||||
&& snapshot.size == 8 + 60
|
||||
&& snapshot.checkSendOrder(secondMessage, 30, 0)
|
||||
&& snapshot.checkSendOrder(firstMessage, 30, 30)
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
bobTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.database
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CrossSigningInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoMetadataEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.GossipingEventEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.IncomingGossipingRequestEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeyInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OlmSessionEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.TrustLevelEntity
|
||||
import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.kotlin.where
|
||||
import timber.log.Timber
|
||||
|
||||
object RealmDebugTools {
|
||||
/**
|
||||
* Log info about the crypto DB
|
||||
*/
|
||||
fun dumpCryptoDb(realmConfiguration: RealmConfiguration) {
|
||||
Realm.getInstance(realmConfiguration).use {
|
||||
Timber.d("Realm located at : ${realmConfiguration.realmDirectory}/${realmConfiguration.realmFileName}")
|
||||
|
||||
val key = realmConfiguration.encryptionKey.joinToString("") { byte -> "%02x".format(byte) }
|
||||
Timber.d("Realm encryption key : $key")
|
||||
|
||||
// Check if we have data
|
||||
Timber.e("Realm is empty: ${it.isEmpty}")
|
||||
|
||||
Timber.d("Realm has CryptoMetadataEntity: ${it.where<CryptoMetadataEntity>().count()}")
|
||||
Timber.d("Realm has CryptoRoomEntity: ${it.where<CryptoRoomEntity>().count()}")
|
||||
Timber.d("Realm has DeviceInfoEntity: ${it.where<DeviceInfoEntity>().count()}")
|
||||
Timber.d("Realm has KeysBackupDataEntity: ${it.where<KeysBackupDataEntity>().count()}")
|
||||
Timber.d("Realm has OlmInboundGroupSessionEntity: ${it.where<OlmInboundGroupSessionEntity>().count()}")
|
||||
Timber.d("Realm has OlmSessionEntity: ${it.where<OlmSessionEntity>().count()}")
|
||||
Timber.d("Realm has UserEntity: ${it.where<UserEntity>().count()}")
|
||||
Timber.d("Realm has KeyInfoEntity: ${it.where<KeyInfoEntity>().count()}")
|
||||
Timber.d("Realm has CrossSigningInfoEntity: ${it.where<CrossSigningInfoEntity>().count()}")
|
||||
Timber.d("Realm has TrustLevelEntity: ${it.where<TrustLevelEntity>().count()}")
|
||||
Timber.d("Realm has GossipingEventEntity: ${it.where<GossipingEventEntity>().count()}")
|
||||
Timber.d("Realm has IncomingGossipingRequestEntity: ${it.where<IncomingGossipingRequestEntity>().count()}")
|
||||
Timber.d("Realm has OutgoingGossipingRequestEntity: ${it.where<OutgoingGossipingRequestEntity>().count()}")
|
||||
Timber.d("Realm has MyDeviceLastSeenInfoEntity: ${it.where<MyDeviceLastSeenInfoEntity>().count()}")
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,4 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="im.vector.matrix.android">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -8,13 +7,20 @@
|
||||
|
||||
<application android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<!--
|
||||
The SDK offers a secured File provider to access downloaded files.
|
||||
Access to these file will be given via the FileService, with a temporary
|
||||
read access permission
|
||||
-->
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
android:name="im.vector.matrix.android.api.session.file.MatrixSDKFileProvider"
|
||||
android:authorities="${applicationId}.mx-sdk.fileprovider"
|
||||
android:exported="false"
|
||||
tools:node="remove" />
|
||||
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/sdk_provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
||||
|
54
matrix-sdk-android/src/main/assets/postMessageAPI.js
Executable file
54
matrix-sdk-android/src/main/assets/postMessageAPI.js
Executable file
@@ -0,0 +1,54 @@
|
||||
var android_widget_events = {};
|
||||
|
||||
var sendObjectMessageToRiotAndroid = function(parameters) {
|
||||
Android.onWidgetEvent(JSON.stringify(parameters));
|
||||
};
|
||||
|
||||
var onWidgetMessageToRiotAndroid = function(event) {
|
||||
/* Use an internal "_id" field for matching onMessage events and requests
|
||||
_id was originally used by the Modular API. Keep it */
|
||||
if (!event.data._id) {
|
||||
/* The Matrix Widget API v2 spec says:
|
||||
"The requestId field should be unique and included in all requests" */
|
||||
event.data._id = event.data.requestId;
|
||||
}
|
||||
/* Make sure to have one id */
|
||||
if (!event.data._id) {
|
||||
event.data._id = Date.now() + "-" + Math.random().toString(36);
|
||||
}
|
||||
|
||||
console.log("onWidgetMessageToRiotAndroid " + event.data._id);
|
||||
|
||||
if (android_widget_events[event.data._id]) {
|
||||
console.log("onWidgetMessageToRiotAndroid : already managed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.origin) {
|
||||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
android_widget_events[event.data._id] = event;
|
||||
|
||||
console.log("onWidgetMessageToRiotAndroid : manage " + event.data);
|
||||
sendObjectMessageToRiotAndroid({'event.data': event.data});
|
||||
};
|
||||
|
||||
var sendResponseFromRiotAndroid = function(eventId, res) {
|
||||
var event = android_widget_events[eventId];
|
||||
|
||||
console.log("sendResponseFromRiotAndroid to " + event.data.action + " for "+ eventId + ": " + JSON.stringify(res));
|
||||
|
||||
var data = JSON.parse(JSON.stringify(event.data));
|
||||
|
||||
data.response = res;
|
||||
|
||||
console.log("sendResponseFromRiotAndroid ---> " + data);
|
||||
|
||||
event.source.postMessage(data, event.origin);
|
||||
android_widget_events[eventId] = true;
|
||||
|
||||
console.log("sendResponseFromRiotAndroid to done");
|
||||
};
|
||||
|
||||
window.addEventListener('message', onWidgetMessageToRiotAndroid, false);
|
@@ -23,7 +23,7 @@ import androidx.work.WorkManager
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.auth.AuthenticationService
|
||||
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
||||
import im.vector.matrix.android.api.legacy.LegacySessionImporter
|
||||
import im.vector.matrix.android.internal.SessionManager
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||
@@ -32,26 +32,17 @@ import im.vector.matrix.android.internal.network.UserAgentHolder
|
||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||
import org.matrix.olm.OlmManager
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
data class MatrixConfiguration(
|
||||
val applicationFlavor: String = "Default-application-flavor",
|
||||
val cryptoConfig: MXCryptoConfig = MXCryptoConfig()
|
||||
) {
|
||||
|
||||
interface Provider {
|
||||
fun providesMatrixConfiguration(): MatrixConfiguration
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the main entry point to the matrix sdk.
|
||||
* To get the singleton instance, use getInstance static method.
|
||||
*/
|
||||
class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) {
|
||||
|
||||
@Inject internal lateinit var legacySessionImporter: LegacySessionImporter
|
||||
@Inject internal lateinit var authenticationService: AuthenticationService
|
||||
@Inject internal lateinit var userAgentHolder: UserAgentHolder
|
||||
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
|
||||
@@ -73,6 +64,10 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
||||
return authenticationService
|
||||
}
|
||||
|
||||
fun legacySessionImporter(): LegacySessionImporter {
|
||||
return legacySessionImporter
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private lateinit var instance: Matrix
|
||||
|
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api
|
||||
|
||||
import im.vector.matrix.android.api.crypto.MXCryptoConfig
|
||||
import java.net.Proxy
|
||||
|
||||
data class MatrixConfiguration(
|
||||
val applicationFlavor: String = "Default-application-flavor",
|
||||
val cryptoConfig: MXCryptoConfig = MXCryptoConfig(),
|
||||
val integrationUIUrl: String = "https://scalar.vector.im/",
|
||||
val integrationRestUrl: String = "https://scalar.vector.im/api",
|
||||
val integrationWidgetUrls: List<String> = listOf(
|
||||
"https://scalar.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar.vector.im/api",
|
||||
"https://scalar-staging.vector.im/_matrix/integrations/v1",
|
||||
"https://scalar-staging.vector.im/api",
|
||||
"https://scalar-staging.riot.im/scalar/api"
|
||||
),
|
||||
/**
|
||||
* Optional proxy to connect to the matrix servers
|
||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
|
||||
*/
|
||||
val proxy: Proxy? = null
|
||||
) {
|
||||
|
||||
/**
|
||||
* Can be implemented by your Application class
|
||||
*/
|
||||
interface Provider {
|
||||
fun providesMatrixConfiguration(): MatrixConfiguration
|
||||
}
|
||||
}
|
@@ -20,7 +20,6 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.auth.data.LoginFlowResult
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.api.auth.login.LoginWizard
|
||||
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
|
||||
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
|
||||
@@ -37,6 +36,11 @@ interface AuthenticationService {
|
||||
*/
|
||||
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable
|
||||
|
||||
/**
|
||||
* Request the supported login flows for the corresponding sessionId.
|
||||
*/
|
||||
fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback<LoginFlowResult>): Cancelable
|
||||
|
||||
/**
|
||||
* Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first.
|
||||
*/
|
||||
@@ -74,15 +78,6 @@ interface AuthenticationService {
|
||||
*/
|
||||
fun getLastAuthenticatedSession(): Session?
|
||||
|
||||
/**
|
||||
* Get an authenticated session. You should at least call authenticate one time before.
|
||||
* If you logout, this session will no longer be valid.
|
||||
*
|
||||
* @param sessionParams the sessionParams to open with.
|
||||
* @return the associated session if any, or null
|
||||
*/
|
||||
fun getSession(sessionParams: SessionParams): Session?
|
||||
|
||||
/**
|
||||
* Create a session after a SSO successful login
|
||||
*/
|
||||
@@ -94,6 +89,7 @@ interface AuthenticationService {
|
||||
* Perform a wellknown request, using the domain from the matrixId
|
||||
*/
|
||||
fun getWellKnownData(matrixId: String,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig?,
|
||||
callback: MatrixCallback<WellknownResult>): Cancelable
|
||||
|
||||
/**
|
||||
|
@@ -32,6 +32,6 @@ const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/"
|
||||
* Path to use when the client want to connect using SSO
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
|
||||
*/
|
||||
const val SSO_FALLBACK_PATH = "/_matrix/client/r0/login/sso/redirect"
|
||||
const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
|
||||
|
||||
const val SSO_REDIRECT_URL_PARAM = "redirectUrl"
|
||||
|
@@ -45,7 +45,7 @@ data class Credentials(
|
||||
* @Deprecated. Clients should extract the server_name from user_id (by splitting at the first colon)
|
||||
* if they require it. Note also that homeserver is not spelt this way.
|
||||
*/
|
||||
@Json(name = "home_server") val homeServer: String,
|
||||
@Json(name = "home_server") val homeServer: String?,
|
||||
/**
|
||||
* ID of the logged-in device. Will be the same as the corresponding parameter in the request, if one was specified.
|
||||
*/
|
||||
|
@@ -20,6 +20,7 @@ import android.net.Uri
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig.Builder
|
||||
import im.vector.matrix.android.internal.network.ssl.Fingerprint
|
||||
import im.vector.matrix.android.internal.util.ensureTrailingSlash
|
||||
import okhttp3.CipherSuite
|
||||
import okhttp3.TlsVersion
|
||||
|
||||
@@ -33,10 +34,10 @@ data class HomeServerConnectionConfig(
|
||||
val homeServerUri: Uri,
|
||||
val identityServerUri: Uri? = null,
|
||||
val antiVirusServerUri: Uri? = null,
|
||||
val allowedFingerprints: MutableList<Fingerprint> = ArrayList(),
|
||||
val allowedFingerprints: List<Fingerprint> = emptyList(),
|
||||
val shouldPin: Boolean = false,
|
||||
val tlsVersions: MutableList<TlsVersion>? = null,
|
||||
val tlsCipherSuites: MutableList<CipherSuite>? = null,
|
||||
val tlsVersions: List<TlsVersion>? = null,
|
||||
val tlsCipherSuites: List<CipherSuite>? = null,
|
||||
val shouldAcceptTlsExtensions: Boolean = true,
|
||||
val allowHttpExtension: Boolean = false,
|
||||
val forceUsageTlsVersions: Boolean = false
|
||||
@@ -68,18 +69,14 @@ data class HomeServerConnectionConfig(
|
||||
*/
|
||||
fun withHomeServerUri(hsUri: Uri): Builder {
|
||||
if (hsUri.scheme != "http" && hsUri.scheme != "https") {
|
||||
throw RuntimeException("Invalid home server URI: " + hsUri)
|
||||
throw RuntimeException("Invalid home server URI: $hsUri")
|
||||
}
|
||||
// ensure trailing /
|
||||
homeServerUri = if (!hsUri.toString().endsWith("/")) {
|
||||
try {
|
||||
val url = hsUri.toString()
|
||||
Uri.parse("$url/")
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Invalid home server URI: $hsUri")
|
||||
}
|
||||
} else {
|
||||
hsUri
|
||||
val hsString = hsUri.toString().ensureTrailingSlash()
|
||||
homeServerUri = try {
|
||||
Uri.parse(hsString)
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Invalid home server URI: $hsUri")
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -97,15 +94,11 @@ data class HomeServerConnectionConfig(
|
||||
throw RuntimeException("Invalid identity server URI: $identityServerUri")
|
||||
}
|
||||
// ensure trailing /
|
||||
if (!identityServerUri.toString().endsWith("/")) {
|
||||
try {
|
||||
val url = identityServerUri.toString()
|
||||
this.identityServerUri = Uri.parse("$url/")
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Invalid identity server URI: $identityServerUri")
|
||||
}
|
||||
} else {
|
||||
this.identityServerUri = identityServerUri
|
||||
val isString = identityServerUri.toString().ensureTrailingSlash()
|
||||
this.identityServerUri = try {
|
||||
Uri.parse(isString)
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException("Invalid identity server URI: $identityServerUri")
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
@@ -16,12 +16,10 @@
|
||||
|
||||
package im.vector.matrix.android.api.auth.data
|
||||
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
|
||||
|
||||
// Either a LoginFlowResponse, or an error if the homeserver is outdated
|
||||
// Either a list of supported login types, or an error if the homeserver is outdated
|
||||
sealed class LoginFlowResult {
|
||||
data class Success(
|
||||
val loginFlowResponse: LoginFlowResponse,
|
||||
val supportedLoginTypes: List<String>,
|
||||
val isLoginAndRegistrationSupported: Boolean,
|
||||
val homeServerUrl: String
|
||||
) : LoginFlowResult()
|
||||
|
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.auth.data
|
||||
package im.vector.matrix.android.api.auth.data
|
||||
|
||||
object LoginFlowTypes {
|
||||
const val PASSWORD = "m.login.password"
|
@@ -21,7 +21,48 @@ package im.vector.matrix.android.api.auth.data
|
||||
* You don't have to manually instantiate it.
|
||||
*/
|
||||
data class SessionParams(
|
||||
/**
|
||||
* Please consider using shortcuts instead
|
||||
*/
|
||||
val credentials: Credentials,
|
||||
|
||||
/**
|
||||
* Please consider using shortcuts instead
|
||||
*/
|
||||
val homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
|
||||
/**
|
||||
* Set to false if the current token is not valid anymore. Application should not have to use this info.
|
||||
*/
|
||||
val isTokenValid: Boolean
|
||||
)
|
||||
) {
|
||||
/*
|
||||
* Shortcuts. Usually the application should only need to use these shortcuts
|
||||
*/
|
||||
|
||||
/**
|
||||
* The userId of the session (Ex: "@user:domain.org")
|
||||
*/
|
||||
val userId = credentials.userId
|
||||
|
||||
/**
|
||||
* The deviceId of the session (Ex: "ABCDEFGH")
|
||||
*/
|
||||
val deviceId = credentials.deviceId
|
||||
|
||||
/**
|
||||
* The current homeserver Url. It can be different that the homeserver url entered
|
||||
* during login phase, because a redirection may have occurred
|
||||
*/
|
||||
val homeServerUrl = homeServerConnectionConfig.homeServerUri.toString()
|
||||
|
||||
/**
|
||||
* The current homeserver host
|
||||
*/
|
||||
val homeServerHost = homeServerConnectionConfig.homeServerUri.host
|
||||
|
||||
/**
|
||||
* The default identity server url if any, returned by the homeserver during login phase
|
||||
*/
|
||||
val defaultIdentityServerUrl = homeServerConnectionConfig.identityServerUri?.toString()
|
||||
}
|
||||
|
@@ -54,30 +54,4 @@ data class WellKnown(
|
||||
|
||||
@Json(name = "m.integrations")
|
||||
val integrations: JsonDict? = null
|
||||
) {
|
||||
/**
|
||||
* Returns the list of integration managers proposed
|
||||
*/
|
||||
fun getIntegrationManagers(): List<WellKnownManagerConfig> {
|
||||
val managers = ArrayList<WellKnownManagerConfig>()
|
||||
integrations?.get("managers")?.let {
|
||||
(it as? ArrayList<*>)?.let { configs ->
|
||||
configs.forEach { config ->
|
||||
(config as? Map<*, *>)?.let { map ->
|
||||
val apiUrl = map["api_url"] as? String
|
||||
val uiUrl = map["ui_url"] as? String ?: apiUrl
|
||||
if (apiUrl != null
|
||||
&& apiUrl.startsWith("https://")
|
||||
&& uiUrl!!.startsWith("https://")) {
|
||||
managers.add(WellKnownManagerConfig(
|
||||
apiUrl = apiUrl,
|
||||
uiUrl = uiUrl
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return managers
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@@ -34,6 +34,12 @@ interface LoginWizard {
|
||||
deviceName: String,
|
||||
callback: MatrixCallback<Session>): Cancelable
|
||||
|
||||
/**
|
||||
* Exchange a login token to an access token
|
||||
*/
|
||||
fun loginWithToken(loginToken: String,
|
||||
callback: MatrixCallback<Session>): Cancelable
|
||||
|
||||
/**
|
||||
* Reset user password
|
||||
*/
|
||||
|
@@ -21,15 +21,12 @@ sealed class Stage(open val mandatory: Boolean) {
|
||||
// m.login.recaptcha
|
||||
data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
|
||||
|
||||
// m.login.oauth2
|
||||
// m.login.email.identity
|
||||
data class Email(override val mandatory: Boolean) : Stage(mandatory)
|
||||
|
||||
// m.login.msisdn
|
||||
data class Msisdn(override val mandatory: Boolean) : Stage(mandatory)
|
||||
|
||||
// m.login.token
|
||||
|
||||
// m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username
|
||||
// and a password, the dummy stage has to be done
|
||||
data class Dummy(override val mandatory: Boolean) : Stage(mandatory)
|
||||
|
@@ -19,11 +19,11 @@ package im.vector.matrix.android.api.crypto
|
||||
/**
|
||||
* Class to define the parameters used to customize or configure the end-to-end crypto.
|
||||
*/
|
||||
data class MXCryptoConfig(
|
||||
data class MXCryptoConfig constructor(
|
||||
// Tell whether the encryption of the event content is enabled for the invited members.
|
||||
// SDK clients can disable this by settings it to false.
|
||||
// Note that the encryption for the invited members will be blocked if the history visibility is "joined".
|
||||
var enableEncryptionForInvitedMembers: Boolean = true,
|
||||
val enableEncryptionForInvitedMembers: Boolean = true,
|
||||
|
||||
/**
|
||||
* If set to true, the SDK will automatically ignore room key request (gossiping)
|
||||
@@ -31,6 +31,5 @@ data class MXCryptoConfig(
|
||||
* If set to false, the request will be forwarded to the application layer; in this
|
||||
* case the application can decide to prompt the user.
|
||||
*/
|
||||
var discardRoomKeyRequestsFromUntrustedDevices : Boolean = true
|
||||
|
||||
val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true
|
||||
)
|
||||
|
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.extensions
|
||||
|
||||
fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence {
|
||||
return when {
|
||||
startsWith(prefix) -> this
|
||||
else -> "$prefix$this"
|
||||
}
|
||||
}
|
@@ -16,10 +16,15 @@
|
||||
|
||||
package im.vector.matrix.android.api.extensions
|
||||
|
||||
inline fun <A> tryThis(operation: () -> A): A? {
|
||||
import timber.log.Timber
|
||||
|
||||
inline fun <A> tryThis(message: String? = null, operation: () -> A): A? {
|
||||
return try {
|
||||
operation()
|
||||
} catch (any: Throwable) {
|
||||
if (message != null) {
|
||||
Timber.e(any, message)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.failure
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.network.ssl.Fingerprint
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
@@ -32,9 +33,11 @@ import java.io.IOException
|
||||
sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
|
||||
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
|
||||
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
|
||||
data class UnrecognizedCertificateFailure(val url: String, val fingerprint: Fingerprint) : Failure()
|
||||
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
|
||||
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
|
||||
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
|
||||
|
||||
// When server send an error, but it cannot be interpreted as a MatrixError
|
||||
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException("HTTP $httpCode: $errorBody"))
|
||||
|
||||
|
@@ -16,8 +16,11 @@
|
||||
|
||||
package im.vector.matrix.android.api.failure
|
||||
|
||||
import im.vector.matrix.android.internal.network.ssl.Fingerprint
|
||||
|
||||
// This class will be sent to the bus
|
||||
sealed class GlobalError {
|
||||
data class InvalidToken(val softLogout: Boolean) : GlobalError()
|
||||
data class ConsentNotGivenError(val consentUri: String) : GlobalError()
|
||||
data class CertificateError(val fingerprint: Fingerprint) : GlobalError()
|
||||
}
|
||||
|
@@ -39,7 +39,10 @@ data class MatrixError(
|
||||
// For M_LIMIT_EXCEEDED
|
||||
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
|
||||
// For M_UNKNOWN_TOKEN
|
||||
@Json(name = "soft_logout") val isSoftLogout: Boolean = false
|
||||
@Json(name = "soft_logout") val isSoftLogout: Boolean = false,
|
||||
// For M_INVALID_PEPPER
|
||||
// {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
|
||||
@Json(name = "lookup_pepper") val newLookupPepper: String? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -129,6 +132,11 @@ data class MatrixError(
|
||||
/** (Not documented yet) */
|
||||
const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
|
||||
const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED"
|
||||
|
||||
// For identity service
|
||||
const val M_INVALID_PEPPER = "M_INVALID_PEPPER"
|
||||
|
||||
// Possible value for "limit_type"
|
||||
const val LIMIT_TYPE_MAU = "monthly_active_user"
|
||||
}
|
||||
|
@@ -1,25 +1,25 @@
|
||||
/*
|
||||
* Copyright 2020 New Vector Ltd
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.powerlevels
|
||||
package im.vector.matrix.android.api.legacy
|
||||
|
||||
object PowerLevelsConstants {
|
||||
interface LegacySessionImporter {
|
||||
|
||||
const val DEFAULT_ROOM_ADMIN_LEVEL = 100
|
||||
const val DEFAULT_ROOM_MODERATOR_LEVEL = 50
|
||||
const val DEFAULT_ROOM_USER_LEVEL = 0
|
||||
/**
|
||||
* Will eventually import a session created by the legacy app.
|
||||
*/
|
||||
fun process()
|
||||
}
|
@@ -87,14 +87,13 @@ class EventMatchCondition(
|
||||
// Very simple glob to regexp converter
|
||||
private fun simpleGlobToRegExp(glob: String): String {
|
||||
var out = "" // "^"
|
||||
for (i in 0 until glob.length) {
|
||||
val c = glob[i]
|
||||
when (c) {
|
||||
for (element in glob) {
|
||||
when (element) {
|
||||
'*' -> out += ".*"
|
||||
'?' -> out += '.'.toString()
|
||||
'.' -> out += "\\."
|
||||
'\\' -> out += "\\\\"
|
||||
else -> out += c
|
||||
else -> out += element
|
||||
}
|
||||
}
|
||||
out += "" // '$'.toString()
|
||||
|
@@ -39,6 +39,6 @@ class SenderNotificationPermissionCondition(
|
||||
|
||||
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
|
||||
val powerLevelsHelper = PowerLevelsHelper(powerLevels)
|
||||
return event.senderId != null && powerLevelsHelper.getUserPowerLevel(event.senderId) >= powerLevelsHelper.notificationLevel(key)
|
||||
return event.senderId != null && powerLevelsHelper.getUserPowerLevelValue(event.senderId) >= powerLevelsHelper.notificationLevel(key)
|
||||
}
|
||||
}
|
||||
|
@@ -25,8 +25,8 @@ sealed class QueryStringValue {
|
||||
object IsNotNull : QueryStringValue()
|
||||
object IsEmpty : QueryStringValue()
|
||||
object IsNotEmpty : QueryStringValue()
|
||||
data class Equals(val string: String, val case: Case) : QueryStringValue()
|
||||
data class Contains(val string: String, val case: Case) : QueryStringValue()
|
||||
data class Equals(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue()
|
||||
data class Contains(val string: String, val case: Case = Case.SENSITIVE) : QueryStringValue()
|
||||
|
||||
enum class Case {
|
||||
SENSITIVE,
|
||||
|
@@ -20,10 +20,13 @@ import androidx.lifecycle.LiveData
|
||||
|
||||
interface InitialSyncProgressService {
|
||||
|
||||
fun getInitialSyncProgressStatus() : LiveData<Status?>
|
||||
fun getInitialSyncProgressStatus(): LiveData<Status>
|
||||
|
||||
data class Status(
|
||||
@StringRes val statusText: Int,
|
||||
val percentProgress: Int = 0
|
||||
)
|
||||
sealed class Status {
|
||||
object Idle : Status()
|
||||
data class Progressing(
|
||||
@StringRes val statusText: Int,
|
||||
val percentProgress: Int = 0
|
||||
) : Status()
|
||||
}
|
||||
}
|
||||
|
@@ -24,12 +24,16 @@ import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||
import im.vector.matrix.android.api.session.account.AccountService
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.call.CallSignalingService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.file.ContentDownloadStateTracker
|
||||
import im.vector.matrix.android.api.session.file.FileService
|
||||
import im.vector.matrix.android.api.session.group.GroupService
|
||||
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
|
||||
import im.vector.matrix.android.api.session.identity.IdentityService
|
||||
import im.vector.matrix.android.api.session.integrationmanager.IntegrationManagerService
|
||||
import im.vector.matrix.android.api.session.profile.ProfileService
|
||||
import im.vector.matrix.android.api.session.pushers.PushersService
|
||||
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
||||
@@ -39,7 +43,11 @@ import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageSer
|
||||
import im.vector.matrix.android.api.session.signout.SignOutService
|
||||
import im.vector.matrix.android.api.session.sync.FilterService
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
import im.vector.matrix.android.api.session.terms.TermsService
|
||||
import im.vector.matrix.android.api.session.typing.TypingUsersTracker
|
||||
import im.vector.matrix.android.api.session.user.UserService
|
||||
import im.vector.matrix.android.api.session.widgets.WidgetService
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
/**
|
||||
* This interface defines interactions with a session.
|
||||
@@ -53,7 +61,7 @@ interface Session :
|
||||
CacheService,
|
||||
SignOutService,
|
||||
FilterService,
|
||||
FileService,
|
||||
TermsService,
|
||||
ProfileService,
|
||||
PushRuleService,
|
||||
PushersService,
|
||||
@@ -77,7 +85,7 @@ interface Session :
|
||||
* Useful shortcut to get access to the userId
|
||||
*/
|
||||
val myUserId: String
|
||||
get() = sessionParams.credentials.userId
|
||||
get() = sessionParams.userId
|
||||
|
||||
/**
|
||||
* The sessionId
|
||||
@@ -120,6 +128,12 @@ interface Session :
|
||||
*/
|
||||
fun getSyncStateLive(): LiveData<SyncState>
|
||||
|
||||
/**
|
||||
* This method returns the current sync state.
|
||||
* @return the current [SyncState].
|
||||
*/
|
||||
fun getSyncState(): SyncState
|
||||
|
||||
/**
|
||||
* This methods return true if an initial sync has been processed
|
||||
*/
|
||||
@@ -140,11 +154,46 @@ interface Session :
|
||||
*/
|
||||
fun contentUploadProgressTracker(): ContentUploadStateTracker
|
||||
|
||||
/**
|
||||
* Returns the TypingUsersTracker associated with the session
|
||||
*/
|
||||
fun typingUsersTracker(): TypingUsersTracker
|
||||
|
||||
/**
|
||||
* Returns the ContentDownloadStateTracker associated with the session
|
||||
*/
|
||||
fun contentDownloadProgressTracker(): ContentDownloadStateTracker
|
||||
|
||||
/**
|
||||
* Returns the cryptoService associated with the session
|
||||
*/
|
||||
fun cryptoService(): CryptoService
|
||||
|
||||
/**
|
||||
* Returns the identity service associated with the session
|
||||
*/
|
||||
fun identityService(): IdentityService
|
||||
|
||||
/**
|
||||
* Returns the widget service associated with the session
|
||||
*/
|
||||
fun widgetService(): WidgetService
|
||||
|
||||
/**
|
||||
* Returns the integration manager service associated with the session
|
||||
*/
|
||||
fun integrationManagerService(): IntegrationManagerService
|
||||
|
||||
/**
|
||||
* Returns the call signaling service associated with the session
|
||||
*/
|
||||
fun callSignalingService(): CallSignalingService
|
||||
|
||||
/**
|
||||
* Returns the file download service associated with the session
|
||||
*/
|
||||
fun fileService(): FileService
|
||||
|
||||
/**
|
||||
* Add a listener to the session.
|
||||
* @param listener the listener to add.
|
||||
@@ -157,6 +206,13 @@ interface Session :
|
||||
*/
|
||||
fun removeListener(listener: Listener)
|
||||
|
||||
/**
|
||||
* Will return a OkHttpClient which will manage pinned certificates and Proxy if configured.
|
||||
* It will not add any access-token to the request.
|
||||
* So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client.
|
||||
*/
|
||||
fun getOkHttpClient(): OkHttpClient
|
||||
|
||||
/**
|
||||
* A global session listener to get notified for some events.
|
||||
*/
|
||||
|
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
interface CallSignalingService {
|
||||
|
||||
fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* Create an outgoing call
|
||||
*/
|
||||
fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall
|
||||
|
||||
fun addCallListener(listener: CallsListener)
|
||||
|
||||
fun removeCallListener(listener: CallsListener)
|
||||
|
||||
fun getCallWithId(callId: String) : MxCall?
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import org.webrtc.PeerConnection
|
||||
|
||||
sealed class CallState {
|
||||
|
||||
/** Idle, setting up objects */
|
||||
object Idle : CallState()
|
||||
|
||||
/** Dialing. Outgoing call is signaling the remote peer */
|
||||
object Dialing : CallState()
|
||||
|
||||
/** Local ringing. Incoming call offer received */
|
||||
object LocalRinging : CallState()
|
||||
|
||||
/** Answering. Incoming call is responding to remote peer */
|
||||
object Answering : CallState()
|
||||
|
||||
/**
|
||||
* Connected. Incoming/Outgoing call, ice layer connecting or connected
|
||||
* Notice that the PeerState failed is not always final, if you switch network, new ice candidtates
|
||||
* could be exchanged, and the connection could go back to connected
|
||||
* */
|
||||
data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState()
|
||||
|
||||
/** Terminated. Incoming/Outgoing call, the call is terminated */
|
||||
object Terminated : CallState()
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
|
||||
interface CallsListener {
|
||||
/**
|
||||
* Called when there is an incoming call within the room.
|
||||
*/
|
||||
fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent)
|
||||
|
||||
fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent)
|
||||
|
||||
/**
|
||||
* An outgoing call is started.
|
||||
*/
|
||||
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent)
|
||||
|
||||
/**
|
||||
* Called when a called has been hung up
|
||||
*/
|
||||
fun onCallHangupReceived(callHangupContent: CallHangupContent)
|
||||
|
||||
fun onCallManagedByOtherSession(callId: String)
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import org.webrtc.EglBase
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The root [EglBase] instance shared by the entire application for
|
||||
* the sake of reducing the utilization of system resources (such as EGL
|
||||
* contexts)
|
||||
* by performing a runtime check.
|
||||
*/
|
||||
object EglUtils {
|
||||
|
||||
// TODO how do we release that?
|
||||
|
||||
/**
|
||||
* Lazily creates and returns the one and only [EglBase] which will
|
||||
* serve as the root for all contexts that are needed.
|
||||
*/
|
||||
@get:Synchronized var rootEglBase: EglBase? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
val configAttributes = EglBase.CONFIG_PLAIN
|
||||
try {
|
||||
field = EglBase.createEgl14(configAttributes)
|
||||
?: EglBase.createEgl10(configAttributes) // Fall back to EglBase10.
|
||||
} catch (ex: Throwable) {
|
||||
Timber.e(ex, "Failed to create EglBase")
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
private set
|
||||
|
||||
val rootEglBaseContext: EglBase.Context?
|
||||
get() {
|
||||
val eglBase = rootEglBase
|
||||
return eglBase?.eglBaseContext
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.SessionDescription
|
||||
|
||||
interface MxCallDetail {
|
||||
val callId: String
|
||||
val isOutgoing: Boolean
|
||||
val roomId: String
|
||||
val otherUserId: String
|
||||
val isVideoCall: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Define both an incoming call and on outgoing call
|
||||
*/
|
||||
interface MxCall : MxCallDetail {
|
||||
|
||||
var state: CallState
|
||||
/**
|
||||
* Pick Up the incoming call
|
||||
* It has no effect on outgoing call
|
||||
*/
|
||||
fun accept(sdp: SessionDescription)
|
||||
|
||||
/**
|
||||
* Reject an incoming call
|
||||
* It's an alias to hangUp
|
||||
*/
|
||||
fun reject() = hangUp()
|
||||
|
||||
/**
|
||||
* End the call
|
||||
*/
|
||||
fun hangUp()
|
||||
|
||||
/**
|
||||
* Start a call
|
||||
* Send offer SDP to the other participant.
|
||||
*/
|
||||
fun offerSdp(sdp: SessionDescription)
|
||||
|
||||
/**
|
||||
* Send Ice candidate to the other participant.
|
||||
*/
|
||||
fun sendLocalIceCandidates(candidates: List<IceCandidate>)
|
||||
|
||||
/**
|
||||
* Send removed ICE candidates to the other participant.
|
||||
*/
|
||||
fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>)
|
||||
|
||||
fun addListener(listener: StateListener)
|
||||
fun removeListener(listener: StateListener)
|
||||
|
||||
interface StateListener {
|
||||
fun onStateUpdate(call: MxCall)
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
// TODO Should not be exposed
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-voip-turnserver
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TurnServerResponse(
|
||||
/**
|
||||
* Required. The username to use.
|
||||
*/
|
||||
@Json(name = "username") val username: String?,
|
||||
|
||||
/**
|
||||
* Required. The password to use.
|
||||
*/
|
||||
@Json(name = "password") val password: String?,
|
||||
|
||||
/**
|
||||
* Required. A list of TURN URIs
|
||||
*/
|
||||
@Json(name = "uris") val uris: List<String>?,
|
||||
|
||||
/**
|
||||
* Required. The time-to-live in seconds
|
||||
*/
|
||||
@Json(name = "ttl") val ttl: Int?
|
||||
)
|
@@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
@@ -99,7 +100,9 @@ interface CryptoService {
|
||||
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
|
||||
|
||||
fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||
|
||||
fun getMyDevicesInfo() : List<DeviceInfo>
|
||||
|
||||
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
|
||||
|
||||
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
|
||||
@@ -138,7 +141,13 @@ interface CryptoService {
|
||||
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest>
|
||||
fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
|
||||
// For testing shared session
|
||||
fun getSharedWithInfo(roomId: String?, sessionId: String) : MXUsersDevicesMap<Int>
|
||||
fun getWithHeldMegolmSession(roomId: String, sessionId: String) : RoomKeyWithHeldContent?
|
||||
}
|
||||
|
@@ -59,7 +59,8 @@ sealed class MXCryptoError : Throwable() {
|
||||
MISSING_PROPERTY,
|
||||
OLM,
|
||||
UNKNOWN_DEVICES,
|
||||
UNKNOWN_MESSAGE_INDEX
|
||||
UNKNOWN_MESSAGE_INDEX,
|
||||
KEYS_WITHHELD
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@@ -41,11 +41,13 @@ interface CrossSigningService {
|
||||
* Users needs to enter credentials
|
||||
*/
|
||||
fun initializeCrossSigning(authParams: UserPasswordAuth?,
|
||||
callback: MatrixCallback<Unit>? = null)
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null
|
||||
|
||||
fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
|
||||
uskKeyPrivateKey: String?,
|
||||
sskPrivateKey: String?) : UserTrustResult
|
||||
sskPrivateKey: String?): UserTrustResult
|
||||
|
||||
fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo?
|
||||
|
||||
@@ -59,6 +61,8 @@ interface CrossSigningService {
|
||||
|
||||
fun canCrossSign(): Boolean
|
||||
|
||||
fun allPrivateKeysKnown(): Boolean
|
||||
|
||||
fun trustUser(otherUserId: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
@@ -74,6 +78,8 @@ interface CrossSigningService {
|
||||
otherDeviceId: String,
|
||||
locallyTrusted: Boolean?): DeviceTrustResult
|
||||
|
||||
// FIXME Those method do not have to be in the service
|
||||
fun onSecretMSKGossip(mskPrivateKey: String)
|
||||
fun onSecretSSKGossip(sskPrivateKey: String)
|
||||
fun onSecretUSKGossip(uskPrivateKey: String)
|
||||
}
|
||||
|
@@ -82,6 +82,9 @@ data class Event(
|
||||
@Transient
|
||||
var mCryptoError: MXCryptoError.ErrorType? = null
|
||||
|
||||
@Transient
|
||||
var mCryptoErrorReason: String? = null
|
||||
|
||||
@Transient
|
||||
var sendState: SendState = SendState.UNKNOWN
|
||||
|
||||
@@ -98,7 +101,7 @@ data class Event(
|
||||
* @return true if event is state event.
|
||||
*/
|
||||
fun isStateEvent(): Boolean {
|
||||
return EventType.isStateEvent(getClearType())
|
||||
return stateKey != null
|
||||
}
|
||||
|
||||
// ==============================================================================================================
|
||||
@@ -162,6 +165,8 @@ data class Event(
|
||||
*/
|
||||
fun isRedactedBySameUser() = senderId == unsignedData?.redactedEvent?.senderId
|
||||
|
||||
fun resolvedPrevContent(): Content? = prevContent ?: unsignedData?.prevContent
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
@@ -180,6 +185,7 @@ data class Event(
|
||||
if (redacts != other.redacts) return false
|
||||
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
||||
if (mCryptoError != other.mCryptoError) return false
|
||||
if (mCryptoErrorReason != other.mCryptoErrorReason) return false
|
||||
if (sendState != other.sendState) return false
|
||||
|
||||
return true
|
||||
@@ -198,6 +204,7 @@ data class Event(
|
||||
result = 31 * result + (redacts?.hashCode() ?: 0)
|
||||
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoErrorReason?.hashCode() ?: 0)
|
||||
result = 31 * result + sendState.hashCode()
|
||||
return result
|
||||
}
|
||||
@@ -220,3 +227,19 @@ fun Event.isImageMessage(): Boolean {
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isVideoMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||
MessageType.MSGTYPE_VIDEO -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isFileMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@@ -38,6 +38,8 @@ object EventType {
|
||||
|
||||
// State Events
|
||||
|
||||
const val STATE_ROOM_WIDGET_LEGACY = "im.vector.modular.widgets"
|
||||
const val STATE_ROOM_WIDGET = "m.widget"
|
||||
const val STATE_ROOM_NAME = "m.room.name"
|
||||
const val STATE_ROOM_TOPIC = "m.room.topic"
|
||||
const val STATE_ROOM_AVATAR = "m.room.avatar"
|
||||
@@ -56,7 +58,6 @@ object EventType {
|
||||
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
||||
|
||||
// Call Events
|
||||
|
||||
const val CALL_INVITE = "m.call.invite"
|
||||
const val CALL_CANDIDATES = "m.call.candidates"
|
||||
const val CALL_ANSWER = "m.call.answer"
|
||||
@@ -65,6 +66,7 @@ object EventType {
|
||||
// Key share events
|
||||
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
||||
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
|
||||
const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld"
|
||||
|
||||
const val REQUEST_SECRET = "m.secret.request"
|
||||
const val SEND_SECRET = "m.secret.send"
|
||||
@@ -84,29 +86,6 @@ object EventType {
|
||||
// Unwedging
|
||||
internal const val DUMMY = "m.dummy"
|
||||
|
||||
private val STATE_EVENTS = listOf(
|
||||
STATE_ROOM_NAME,
|
||||
STATE_ROOM_TOPIC,
|
||||
STATE_ROOM_AVATAR,
|
||||
STATE_ROOM_MEMBER,
|
||||
STATE_ROOM_THIRD_PARTY_INVITE,
|
||||
STATE_ROOM_CREATE,
|
||||
STATE_ROOM_JOIN_RULES,
|
||||
STATE_ROOM_GUEST_ACCESS,
|
||||
STATE_ROOM_POWER_LEVELS,
|
||||
STATE_ROOM_ALIASES,
|
||||
STATE_ROOM_TOMBSTONE,
|
||||
STATE_ROOM_CANONICAL_ALIAS,
|
||||
STATE_ROOM_HISTORY_VISIBILITY,
|
||||
STATE_ROOM_RELATED_GROUPS,
|
||||
STATE_ROOM_PINNED_EVENT,
|
||||
STATE_ROOM_ENCRYPTION
|
||||
)
|
||||
|
||||
fun isStateEvent(type: String): Boolean {
|
||||
return STATE_EVENTS.contains(type)
|
||||
}
|
||||
|
||||
fun isCallEvent(type: String): Boolean {
|
||||
return type == CALL_INVITE
|
||||
|| type == CALL_CANDIDATES
|
||||
|
@@ -26,5 +26,5 @@ object RelationType {
|
||||
/** Lets you define an event which references an existing event.*/
|
||||
const val REFERENCE = "m.reference"
|
||||
/** Lets you define an event which adds a response to an existing event.*/
|
||||
const val RESPONSE = "m.response"
|
||||
const val RESPONSE = "org.matrix.response"
|
||||
}
|
||||
|
@@ -39,5 +39,10 @@ data class UnsignedData(
|
||||
* Optional. The previous content for this event. If there is no previous content, this key will be missing.
|
||||
*/
|
||||
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null,
|
||||
@Json(name = "m.relations") val relations: AggregatedRelations? = null
|
||||
@Json(name = "m.relations") val relations: AggregatedRelations? = null,
|
||||
/**
|
||||
* Optional. The eventId of the previous state event being replaced.
|
||||
*/
|
||||
@Json(name = "replaces_state") val replacesState: String? = null
|
||||
|
||||
)
|
||||
|
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.file
|
||||
|
||||
interface ContentDownloadStateTracker {
|
||||
fun track(key: String, updateListener: UpdateListener)
|
||||
fun unTrack(key: String, updateListener: UpdateListener)
|
||||
fun clear()
|
||||
|
||||
sealed class State {
|
||||
object Idle : State()
|
||||
data class Downloading(val current: Long, val total: Long, val indeterminate: Boolean) : State()
|
||||
object Decrypting : State()
|
||||
object Success : State()
|
||||
data class Failure(val errorCode: Int) : State()
|
||||
}
|
||||
|
||||
interface UpdateListener {
|
||||
fun onDownloadStateUpdate(state: State)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user