mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 00:02:48 +02:00
Compare commits
682 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ee1d5faf0d | ||
|
7308c4a15c | ||
|
db6045778e | ||
|
2810923519 | ||
|
7fddfa45e8 | ||
|
258acaf814 | ||
|
2f6cdc6529 | ||
|
1f226ca6c7 | ||
|
c90f5ed370 | ||
|
ccb466edbe | ||
|
1204f6d9ce | ||
|
562cfce9e2 | ||
|
b732ea6c69 | ||
|
a048f79b37 | ||
|
3214f7d3d5 | ||
|
06ef665f66 | ||
|
e1a07f1da6 | ||
|
5c32c7388a | ||
|
c04f22d3bf | ||
|
cbf43ea7b3 | ||
|
9197275343 | ||
|
cc4298209b | ||
|
f26ce64914 | ||
|
e3f5b7cab3 | ||
|
dde9cdd8ac | ||
|
0d0308d584 | ||
|
a47ff99be7 | ||
|
e619217687 | ||
|
6f5b3371fe | ||
|
f7b9fc3bb1 | ||
|
700a2e9ce3 | ||
|
5176a3e2aa | ||
|
4631cd0e80 | ||
|
60c6512418 | ||
|
12bc821ef3 | ||
|
c82e910c38 | ||
|
7ae0620b20 | ||
|
de32cdb703 | ||
|
12e2a8ffc8 | ||
|
833f64fcec | ||
|
73c051d2b1 | ||
|
da06695ab7 | ||
|
984ffc4fb9 | ||
|
d7b44ba0cf | ||
|
e90fe5cf28 | ||
|
20c7d80bb1 | ||
|
25f8a9418a | ||
|
21b609420b | ||
|
794b89c041 | ||
|
407595e613 | ||
|
cac8099117 | ||
|
4cc3e87d64 | ||
|
3e429490e7 | ||
|
e23b02f2e2 | ||
|
aa5ae45a0c | ||
|
d7558902f7 | ||
|
aa5de1896f | ||
|
e5596f6a97 | ||
|
d638306b04 | ||
|
dba9356472 | ||
|
fb247f8bea | ||
|
c880e2b848 | ||
|
1436477a14 | ||
|
35fe4f7268 | ||
|
a488e88f93 | ||
|
0d51c160eb | ||
|
27207a27ae | ||
|
5082defddf | ||
|
6e0095edb0 | ||
|
d78cd81c6f | ||
|
ef93ef57f9 | ||
|
a600e896a0 | ||
|
993c340c4f | ||
|
87f506f23b | ||
|
44fb53ce6b | ||
|
1d2e374526 | ||
|
5c423d2897 | ||
|
6d0a1ee824 | ||
|
978ab76644 | ||
|
da4e3f3b9c | ||
|
f6d2d05e70 | ||
|
d747d96e77 | ||
|
484b69165b | ||
|
0159e6dc00 | ||
|
8f7aae4980 | ||
|
0f9e26efc5 | ||
|
53f0b77fb0 | ||
|
fca669ff98 | ||
|
e910fc60fe | ||
|
1d2e62fed7 | ||
|
7651eb5c3c | ||
|
a06484c260 | ||
|
9b49e6a817 | ||
|
bef0c7725b | ||
|
7237ec7310 | ||
|
3abe5b8467 | ||
|
0406854fa5 | ||
|
ca6a398a72 | ||
|
5e81ce8e3e | ||
|
8658090736 | ||
|
614ac88567 | ||
|
797dcdb48b | ||
|
3a696ea9a1 | ||
|
1b19cb7449 | ||
|
ea81a5298b | ||
|
ae65eb6a3e | ||
|
67e07a7932 | ||
|
6ef15f3c1c | ||
|
0e3be83da2 | ||
|
8b5b7db241 | ||
|
1fd8a5fa91 | ||
|
34d7a86175 | ||
|
21f1f89a4b | ||
|
159f286b2c | ||
|
06454cc3e0 | ||
|
be27b580d6 | ||
|
f91b6938f5 | ||
|
a7fea8d012 | ||
|
18bc40fb66 | ||
|
1dec4bc96b | ||
|
0e28214b63 | ||
|
ca61751a8b | ||
|
cce1c2252d | ||
|
6a4d887941 | ||
|
4de1759321 | ||
|
139cd051ab | ||
|
33b2abc3b9 | ||
|
c63128cfbd | ||
|
5063188b25 | ||
|
b9f0c176d9 | ||
|
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 | ||
|
bbbd45efcd | ||
|
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 | ||
|
0bc51b2ed8 | ||
|
4741169cc7 | ||
|
a8ad57a9b0 | ||
|
8582ad6015 | ||
|
51898a8109 | ||
|
d63f00851a | ||
|
f179fc523d | ||
|
eda29e3fef | ||
|
633548f190 | ||
|
811cbb2e20 | ||
|
6569ee5d10 | ||
|
5f60d7fd3b | ||
|
10f8aebde2 | ||
|
ea771476cc | ||
|
08bc487f17 | ||
|
1b6b71ed98 | ||
|
9f2631110e | ||
|
e8b1e418fa | ||
|
44563e73e2 | ||
|
d3595173b4 | ||
|
14d4b34cee | ||
|
538149233b | ||
|
bcb203f8e0 | ||
|
3c6937ff5a | ||
|
6c0f775c4b | ||
|
ea3e467dc4 | ||
|
5a65eddf59 | ||
|
e979bee920 | ||
|
eff08955f1 | ||
|
9310073c07 | ||
|
28869f4382 | ||
|
cd949e9d38 | ||
|
541e1fc4cc | ||
|
93fe00a299 | ||
|
8814364497 | ||
|
0d9ff4bde3 | ||
|
9c595b6c02 | ||
|
d49d0295a2 | ||
|
da7c971927 | ||
|
548879bd9f | ||
|
0c2516ccf8 | ||
|
332f227bc1 | ||
|
a98b2ecce3 | ||
|
195e2703b9 | ||
|
c1f1620624 | ||
|
f5284e8447 | ||
|
347cf08861 | ||
|
540317639a | ||
|
bdcd96544e | ||
|
8237c949f1 | ||
|
e38cb7c1a6 | ||
|
868d9cf55c | ||
|
aa3e68f3fd | ||
|
bf2d937ad6 | ||
|
e24d5b3ca4 | ||
|
e9778d6feb | ||
|
8c4c909f44 | ||
|
a1db8653ab | ||
|
cc5df1e1d5 | ||
|
87b1394e98 | ||
|
e3c2af2c59 | ||
|
a618a9214e | ||
|
76133ab55e | ||
|
2d4a728af4 | ||
|
4a2a6d34ae | ||
|
75c2dfcd48 | ||
|
6ebedaf540 | ||
|
85e8e652f1 | ||
|
f025811025 | ||
|
3aabb17ea5 | ||
|
f1e5129acb | ||
|
e8dbed1642 | ||
|
6d270dc5f4 | ||
|
2afe642e8b | ||
|
680e62cb98 | ||
|
c5ba746904 | ||
|
702711fc5e | ||
|
0855806ae2 | ||
|
a2c75e7c71 | ||
|
6f996f1f09 | ||
|
ac7a929a1a | ||
|
8313e45737 | ||
|
09ca2361d7 | ||
|
bcfd322b85 | ||
|
9dc831d8e5 | ||
|
9514835232 | ||
|
e93a2d7c5d | ||
|
b2f6476f78 | ||
|
b7d86c3fa4 | ||
|
89506b9e81 | ||
|
51abdb6066 | ||
|
9e60f73bcf | ||
|
1e6d98a121 | ||
|
98d56cb556 | ||
|
8b1a07b8a8 | ||
|
92e809fa6d | ||
|
a0998e4aff | ||
|
804d712848 | ||
|
08cda2ee10 | ||
|
bf03b367f1 | ||
|
c1da4aecd7 | ||
|
38c54e0f2c | ||
|
9ebf87df62 | ||
|
32d2cea7f8 | ||
|
f998cb6b18 | ||
|
9d4e903c4a | ||
|
cfdf5cb552 | ||
|
e859357c6a | ||
|
e7f13c9efe | ||
|
2a68c8d08b | ||
|
b1c088a03b | ||
|
04f0146afd | ||
|
92ecfafa0d | ||
|
f74ab2dfd4 | ||
|
a1dc383148 | ||
|
b853397c0a | ||
|
0cfd33fc8b | ||
|
51e63c5d1d | ||
|
e9ba7342d5 | ||
|
7434aed43f | ||
|
283f32479d | ||
|
f4057ea3fa | ||
|
a7480c1860 | ||
|
c4ad90696f | ||
|
85ee183e4f | ||
|
586b0fe6e2 | ||
|
bdfce35d9a | ||
|
3648d6292a | ||
|
b8f88d323d | ||
|
b8f66a36d5 | ||
|
d4050a7b9d | ||
|
bfb8b6203c | ||
|
15223ecfe4 | ||
|
e098b87d0a | ||
|
e878821df2 | ||
|
596fcf94ba | ||
|
332041e13b | ||
|
5008bfd6a9 | ||
|
2ef82f1b82 | ||
|
2f6b38eb39 | ||
|
69eaf2695e | ||
|
542d11d7f1 | ||
|
1de819b0a3 | ||
|
5713fa4f59 | ||
|
93fb40f323 | ||
|
f4314ebdae | ||
|
7b075f138d | ||
|
c62a7c4051 | ||
|
3e97e03ccc | ||
|
74a3d7619b | ||
|
fc88892ee6 | ||
|
01e42838ef | ||
|
192c6db03e | ||
|
a98bd0940f | ||
|
c0623726ae | ||
|
ba8ec97f6a | ||
|
fb1c01c37c | ||
|
f8e35da533 | ||
|
53053d8f4a | ||
|
9eab1acf1e | ||
|
7aa9f88ceb | ||
|
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 | ||
|
08710978c5 | ||
|
cba9109206 | ||
|
482621e86f | ||
|
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 | ||
|
23cbed310a | ||
|
1786ba30f7 |
1
.idea/dictionaries/bmarty.xml
generated
1
.idea/dictionaries/bmarty.xml
generated
@@ -7,6 +7,7 @@
|
||||
<w>ciphertext</w>
|
||||
<w>coroutine</w>
|
||||
<w>decryptor</w>
|
||||
<w>displayname</w>
|
||||
<w>emoji</w>
|
||||
<w>emojis</w>
|
||||
<w>fdroid</w>
|
||||
|
@@ -28,8 +28,8 @@ Even if we try to be able to work on all the functionalities, we have more knowl
|
||||
|
||||
# Other contributors
|
||||
|
||||
First of all, we thank all contributors who use RiotX and report problems on this GitHub project or via the integrated rageshake function.
|
||||
First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function.
|
||||
|
||||
We do not forget all translators, for their work of translating RiotX into many languages. They are also the authors of RiotX.
|
||||
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
|
||||
|
||||
Feel free to add your name below, when you contribute to the project!
|
||||
|
164
CHANGES.md
164
CHANGES.md
@@ -1,3 +1,165 @@
|
||||
Changes in Element 1.0.4 (2020-08-03)
|
||||
===================================================
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix Crash when opening invite to room user screen
|
||||
|
||||
Changes in Element 1.0.3 (2020-07-31)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Support server admin option to disable E2EE for DMs / private rooms [users can still enable] (#1794)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Crash reported on playstore for HomeActivity launch (151 reports)
|
||||
|
||||
Changes in Element 1.0.2 (2020-07-29)
|
||||
===================================================
|
||||
|
||||
Improvements 🙌:
|
||||
- Added Session Database migration to avoid unneeded initial syncs
|
||||
|
||||
Changes in Element 1.0.1 (2020-07-28)
|
||||
===================================================
|
||||
|
||||
Improvements 🙌:
|
||||
- Sending events is now retried only 3 times, so we avoid blocking the sending queue too long.
|
||||
- Display warning when fail to send events in room list
|
||||
- Improve UI of edit role action in member profile
|
||||
- Moderation | New screen to display list of banned users in room settings, with unban action
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix theme issue on Room directory screen (#1613)
|
||||
- Fix notification not dismissing when entering a room
|
||||
- Fix uploads don't work with Room v6 (#1558)
|
||||
- Fix Requesting avatar thumbnails in Element uses wrong http "user-agent" string (#1725)
|
||||
- Fix 404 on EMS (#1761)
|
||||
- Fix Infinite loop at startup when migrating account from Riot (#1699)
|
||||
- Fix Element crashes in loop after initial sync (#1709)
|
||||
- Remove inner mx-reply tags before replying
|
||||
- Fix timeline items not loading when there are only filtered events
|
||||
- Fix "Voice & Video" grayed out in Settings (#1733)
|
||||
- Fix Allow VOIP call in all rooms with 2 participants (even if not DM)
|
||||
- Migration from old client does not enable notifications (#1723)
|
||||
|
||||
Other changes:
|
||||
- i18n deactivated account error
|
||||
|
||||
Changes in Element 1.0.0 (2020-07-15)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Re-branding: The app is now called Element. New name, new themes, new icons, etc. More details here: https://element.io/blog/welcome-to-element/ (#1691)
|
||||
|
||||
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)
|
||||
===================================================
|
||||
|
||||
@@ -591,7 +753,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a
|
||||
=======================================================
|
||||
|
||||
|
||||
Changes in RiotX 0.X.0 (2020-XX-XX)
|
||||
Changes in Element 1.X.X (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
|
@@ -2,9 +2,7 @@
|
||||
|
||||
Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md
|
||||
|
||||
Android support can be found in this [](https://matrix.to/#/#riot-android:matrix.org) room.
|
||||
|
||||
Dedicated room for RiotX: [](https://matrix.to/#/#riotx:matrix.org)
|
||||
Android support can be found in this [](https://matrix.to/#/#element-android:matrix.org) room.
|
||||
|
||||
# Specific rules for Matrix Android projects
|
||||
|
||||
@@ -19,7 +17,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,21 +29,19 @@ 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.
|
||||
|
||||
## Compilation
|
||||
|
||||
For now, the Matrix SDK and the RiotX application are in the same project. So there is no specific thing to do, this project should compile without any special action.
|
||||
For now, the Matrix SDK and the Element application are in the same project. So there is no specific thing to do, this project should compile without any special action.
|
||||
|
||||
## I want to help translating RiotX
|
||||
## I want to help translating Element
|
||||
|
||||
If you want to fix an issue with an English string, please submit a PR.
|
||||
If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please use [Weblate](https://translate.riot.im/projects/riot-android/).
|
||||
|
||||
For the moment, Strings from Riot will be used, there is no dedicated project in Weblate for RiotX.
|
||||
|
||||
## I want to submit a PR to fix an issue
|
||||
|
||||
Please check if a corresponding issue exists. If yes, please let us know in a comment that you're working on it.
|
||||
@@ -97,7 +97,7 @@ Make sure the following commands execute without any error:
|
||||
|
||||
### Tests
|
||||
|
||||
RiotX is currently supported on Android KitKat (API 19+): please test your change on an Android device (or Android emulator) running with API 19. Many issues can happen (including crashes) on older devices.
|
||||
Element is currently supported on Android Lollipop (API 21+): please test your change on an Android device (or Android emulator) running with API 21. Many issues can happen (including crashes) on older devices.
|
||||
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
|
||||
|
||||
You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment.
|
||||
@@ -116,7 +116,7 @@ Please consider accessibility as an important point. As a minimum requirement, i
|
||||
When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language.
|
||||
You can check this in the layout editor preview by selecting any RTL language (ex: Arabic).
|
||||
|
||||
Also please check that the colors are ok for all the current themes of RiotX. Please use `?attr` instead of `@color` to reference colors in the layout. You can check this in the layout editor preview by selecting all the main themes (`AppTheme.Status`, `AppTheme.Dark`, etc.).
|
||||
Also please check that the colors are ok for all the current themes of Element. Please use `?attr` instead of `@color` to reference colors in the layout. You can check this in the layout editor preview by selecting all the main themes (`AppTheme.Status`, `AppTheme.Dark`, etc.).
|
||||
|
||||
### Authors
|
||||
|
||||
|
26
README.md
26
README.md
@@ -1,38 +1,32 @@
|
||||
[](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
[](https://translate.riot.im/engage/riot-android/?utm_source=widget)
|
||||
[](https://matrix.to/#/#riotx:matrix.org)
|
||||
[](https://matrix.to/#/#element-android:matrix.org)
|
||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
|
||||
# RiotX Android
|
||||
# Element Android
|
||||
|
||||
RiotX is an Android Matrix Client currently in beta but in active development.
|
||||
Element Android is an Android Matrix Client provided by [Element](https://element.io/).
|
||||
|
||||
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.
|
||||
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience.
|
||||
|
||||
[<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.app)
|
||||
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
|
||||
|
||||
Nightly build: [](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
|
||||
# New Android SDK
|
||||
|
||||
RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the SDK is stable enough.
|
||||
|
||||
Element is based on a new Android SDK fully written in Kotlin (like Element). In order to make the early development as fast as possible, Element and the new SDK currently share the same git repository. We will make separate repos once the SDK is stable enough.
|
||||
|
||||
# Roadmap
|
||||
|
||||
The current target is to release an application out of beta with the same level of features (and even more) as Riot.
|
||||
The roadmap has 3 phases:
|
||||
|
||||
- [phase 0](https://github.com/vector-im/riotX-android/labels/phase0): Prototyping / Project setup
|
||||
- [phase 1](https://github.com/vector-im/riotX-android/labels/phase1): Beta release to the Play Store
|
||||
- [phase 2](https://github.com/vector-im/riotX-android/labels/phase2): Out of beta
|
||||
|
||||
The version 1.0.0 of Element still misses some features which was previously included in Riot-Android.
|
||||
The team will work to add them on a regular basis.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
|
||||
|
||||
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#riotx:matrix.org).
|
||||
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org).
|
||||
|
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 {
|
||||
|
@@ -14,7 +14,7 @@ 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 SDK supports incremental sendAttempt (this is not used by Element)
|
||||
- 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.
|
||||
@@ -22,7 +22,7 @@ Difference though (list not exhaustive):
|
||||
|
||||
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)
|
||||
- Add email or phone to account (not P1, can be done on Element-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.
|
||||
@@ -55,7 +55,7 @@ The list can be found here: https://matrix.org/blog/2019/09/27/privacy-improveme
|
||||
|
||||
- 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)
|
||||
- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) Element 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.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
This document aims to describe how RiotX android displays notifications to the end user. It also clarifies notifications and background settings in the app.
|
||||
This document aims to describe how Element android displays notifications to the end user. It also clarifies notifications and background settings in the app.
|
||||
|
||||
# Table of Contents
|
||||
1. [Prerequisites Knowledge](#prerequisites-knowledge)
|
||||
@@ -9,7 +9,7 @@ This document aims to describe how RiotX android displays notifications to the e
|
||||
* [How does the Home Server knows when to notify a client?](#how-does-the-home-server-knows-when-to-notify-a-client)
|
||||
* [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation)
|
||||
* [Background processing limitations](#background-processing-limitations)
|
||||
2. [RiotX Notification implementations](#riotx-notification-implementations)
|
||||
2. [Element Notification implementations](#element-notification-implementations)
|
||||
* [Requirements](#requirements)
|
||||
* [Foreground sync mode (Gplay & F-Droid)](#foreground-sync-mode-gplay-f-droid)
|
||||
* [Push (FCM) received in background](#push-fcm-received-in-background)
|
||||
@@ -50,7 +50,7 @@ By default, this is 0, so the server will return immediately even if the respons
|
||||
|
||||
**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync.
|
||||
|
||||
When the RiotX Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
|
||||
When the Element Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
|
||||
|
||||
## How does a mobile app receives push notification
|
||||
|
||||
@@ -86,7 +86,7 @@ This need some disambiguation, because it is the source of common confusion:
|
||||
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
|
||||
This server is called a **Push Gateway** in the matrix world
|
||||
|
||||
That means that RiotX Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
|
||||
That means that Element Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
|
||||
|
||||
If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app.
|
||||
|
||||
@@ -132,7 +132,7 @@ A Home Server can be configured with default rules (for Direct messages, group m
|
||||
|
||||
There are different kind of push rules, it can be per room (each new message on this room should be notified), it can also define a pattern that a message should match (when you are mentioned, or key word based).
|
||||
|
||||
Notifications have 2 'levels' (`highlighted = true/false sound = default/custom`). In RiotX these notifications level are reflected as Noisy/Silent.
|
||||
Notifications have 2 'levels' (`highlighted = true/false sound = default/custom`). In Element these notifications level are reflected as Noisy/Silent.
|
||||
|
||||
**What about encrypted messages?**
|
||||
|
||||
@@ -158,7 +158,7 @@ In a nutshell, apps can't do much in background now.
|
||||
|
||||
If the devices is not plugged and stays IDLE for a certain amount of time, radio (mobile connectivity) and CPU can/will be turned off.
|
||||
|
||||
For an application like RiotX, where users can receive important information at anytime, the best option is to rely on a push system (Google's Firebase Message a.k.a FCM). FCM high priority push can wake up the device and whitelist an application to perform background task (for a limited but unspecified amount of time).
|
||||
For an application like Element, where users can receive important information at anytime, the best option is to rely on a push system (Google's Firebase Message a.k.a FCM). FCM high priority push can wake up the device and whitelist an application to perform background task (for a limited but unspecified amount of time).
|
||||
|
||||
Notice that this is still evolving, and in future versions application that has been 'background restricted' by users won't be able to wake up even when a high priority push is received. Also high priority notifications could be rate limited (not defined anywhere)
|
||||
|
||||
@@ -167,41 +167,41 @@ The documentation on this subject is vague, and as per our experiments not alway
|
||||
|
||||
It is getting more and more complex to have reliable notifications when FCM is not used.
|
||||
|
||||
# RiotX Notification implementations
|
||||
# Element Notification implementations
|
||||
|
||||
## Requirements
|
||||
|
||||
RiotX Android must work with and without FCM.
|
||||
* The RiotX android app published on F-Droid do not rely on FCM (all related dependencies are not present)
|
||||
* The RiotX android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services)
|
||||
Element Android must work with and without FCM.
|
||||
* The Element android app published on F-Droid do not rely on FCM (all related dependencies are not present)
|
||||
* The Element android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services)
|
||||
|
||||
## Foreground sync mode (Gplay & F-Droid)
|
||||
|
||||
When in foreground, RiotX performs sync continuously with a timeout value set to 10 seconds (see HttpPooling).
|
||||
When in foreground, Element performs sync continuously with a timeout value set to 10 seconds (see HttpPooling).
|
||||
|
||||
As this mode does not need to live beyond the scope of the application, and as per Google recommendation, RiotX uses the internal app resources (Thread and Timers) to perform the syncs.
|
||||
As this mode does not need to live beyond the scope of the application, and as per Google recommendation, Element uses the internal app resources (Thread and Timers) to perform the syncs.
|
||||
|
||||
This mode is turned on when the app enters foreground, and off when enters background.
|
||||
|
||||
In background, and depending on wether push is available or not, RiotX will use different methods to perform the syncs (Workers / Alarms / Service)
|
||||
In background, and depending on wether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service)
|
||||
|
||||
## Push (FCM) received in background
|
||||
|
||||
In order to enable Push, RiotX must first get a push token from the firebase SDK, then register a pusher with this token on the HomeServer.
|
||||
In order to enable Push, Element must first get a push token from the firebase SDK, then register a pusher with this token on the HomeServer.
|
||||
|
||||
When a message should be notified to a user, the user's homeserver notifies the registered `push gateway` for RiotX, that is [sygnal](https://github.com/matrix-org/sygnal) _- The reference implementation for push gateways -_ hosted by matrix.org.
|
||||
When a message should be notified to a user, the user's homeserver notifies the registered `push gateway` for Element, that is [sygnal](https://github.com/matrix-org/sygnal) _- The reference implementation for push gateways -_ hosted by matrix.org.
|
||||
|
||||
This sygnal instance is configured with the required FCM API authentication token, and will then use the FCM API in order to notify the user's device running riotX.
|
||||
This sygnal instance is configured with the required FCM API authentication token, and will then use the FCM API in order to notify the user's device running Element.
|
||||
|
||||
```
|
||||
Homeserver ----> Sygnal (configured for RiotX) ----> FCM ----> RiotX
|
||||
Homeserver ----> Sygnal (configured for Element) ----> FCM ----> Element
|
||||
```
|
||||
|
||||
The push gateway is configured to only send `(eventId,roomId)` in the push payload (for better [privacy](#push-vs-privacy-and-mitigation)).
|
||||
|
||||
RiotX needs then to synchronise with the user's HomeServer, in order to resolve the event and create a notification.
|
||||
Element needs then to synchronise with the user's HomeServer, in order to resolve the event and create a notification.
|
||||
|
||||
As per [Google recommendation](https://android-developers.googleblog.com/2018/09/notifying-your-users-with-fcm.html), RiotX will then use the WorkManager API in order to trigger a background sync.
|
||||
As per [Google recommendation](https://android-developers.googleblog.com/2018/09/notifying-your-users-with-fcm.html), Element will then use the WorkManager API in order to trigger a background sync.
|
||||
|
||||
**Google recommendations:**
|
||||
> We recommend using FCM messages in combination with the WorkManager 1 or JobScheduler API
|
||||
@@ -209,7 +209,7 @@ As per [Google recommendation](https://android-developers.googleblog.com/2018/09
|
||||
> Avoid background services. One common pitfall is using a background service to fetch data in the FCM message handler, since background service will be stopped by the system per recent changes to Google Play Policy
|
||||
|
||||
```
|
||||
Homeserver ----> Sygnal ----> FCM ----> RiotX
|
||||
Homeserver ----> Sygnal ----> FCM ----> Element
|
||||
(Sync) ----> Homeserver
|
||||
<----
|
||||
Display notification
|
||||
@@ -217,24 +217,24 @@ Homeserver ----> Sygnal ----> FCM ----> RiotX
|
||||
|
||||
**Possible outcomes**
|
||||
|
||||
Upon reception of the FCM push, RiotX will perform a sync call to the Home Server, during this process it is possible that:
|
||||
Upon reception of the FCM push, Element will perform a sync call to the Home Server, during this process it is possible that:
|
||||
* Happy path, the sync is performed, the message resolved and displayed in the notification drawer
|
||||
* The notified message is not in the sync. Can happen if a lot of things did happen since the push (`gappy sync`)
|
||||
* The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally)
|
||||
* The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails.
|
||||
|
||||
RiotX implements several strategies in these cases (TODO document)
|
||||
Element implements several strategies in these cases (TODO document)
|
||||
|
||||
## FCM Fallback mode
|
||||
|
||||
It is possible that RiotX is not able to get a FCM push token.
|
||||
It is possible that Element is not able to get a FCM push token.
|
||||
Common errors (amoung several others) that can cause that:
|
||||
* Google Play Services is outdated
|
||||
* Google Play Service fails in someways with FCM servers (infamous `SERVICE_NOT_AVAILABLE`)
|
||||
|
||||
If RiotX is able to detect one of this cases, it will notifies it to the users and when possible help him fix it via a dedicated troubleshoot screen.
|
||||
If Element is able to detect one of this cases, it will notifies it to the users and when possible help him fix it via a dedicated troubleshoot screen.
|
||||
|
||||
Meanwhile, in order to offer a minimal service, and as per Google's recommendation for background activities, RiotX will launch periodic background sync in order to stays in sync with servers.
|
||||
Meanwhile, in order to offer a minimal service, and as per Google's recommendation for background activities, Element will launch periodic background sync in order to stays in sync with servers.
|
||||
|
||||
The fallback mode is impacted by all the battery life saving mechanism implemented by android. Meaning that if the app is not used for a certain amount of time (`App-Standby`), or the device stays still and unplugged (`Light Doze`) , the sync will become less frequent.
|
||||
|
||||
@@ -248,7 +248,7 @@ The fallback mode is supposed to be a temporary state waiting for the user to fi
|
||||
|
||||
## F-Droid background Mode
|
||||
|
||||
The F-Droid RiotX flavor has no dependencies to FCM, therefore cannot relies on Push.
|
||||
The F-Droid Element flavor has no dependencies to FCM, therefore cannot relies on Push.
|
||||
|
||||
Also Google's recommended background processing method cannot be applied. This is because all of these methods are affected by IDLE modes, and will result on the user not being notified at all when the app is in a Doze mode (only in maintenance windows that could happens only after hours).
|
||||
|
||||
@@ -262,7 +262,7 @@ F-Droid version will schedule alarms that will then trigger a Broadcast Receiver
|
||||
|
||||
Depending on the system status (or device make), it is still possible that the app is not given enough time to launch the service, or that the radio is still turned off thus preventing the sync to success (that's why Alarms are not recommended for network related tasks).
|
||||
|
||||
That is why on RiotX F-Droid, the broadcast receiver will acquire a temporary WAKE_LOCK for several seconds (thus securing cpu/network), and launch the service in foreground. The service performs the sync.
|
||||
That is why on Element F-Droid, the broadcast receiver will acquire a temporary WAKE_LOCK for several seconds (thus securing cpu/network), and launch the service in foreground. The service performs the sync.
|
||||
|
||||
Note that foreground services require to put a notification informing the user that the app is doing something even if not launched).
|
||||
|
||||
|
@@ -132,7 +132,7 @@ It's worth noting that the response from the homeserver contains the userId of A
|
||||
|
||||
### Login with Msisdn
|
||||
|
||||
Not supported yet in RiotX
|
||||
Not supported yet in Element
|
||||
|
||||
### Login with SSO
|
||||
|
||||
@@ -155,9 +155,9 @@ Not supported yet in RiotX
|
||||
|
||||
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
|
||||
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=element%3A%2F%element
|
||||
|
||||
The parameter `redirectUrl` is set to `riotx://riotx`.
|
||||
The parameter `redirectUrl` is set to `element://element`.
|
||||
|
||||
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)
|
||||
|
||||
@@ -167,9 +167,9 @@ During the process, user may be asked to validate an email by clicking on a link
|
||||
|
||||
Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken`
|
||||
|
||||
> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
|
||||
> element://element?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
|
||||
This navigation is intercepted by Element 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'
|
||||
|
||||
|
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 │ │ │ │
|
||||
│ │ │ └────────────────────┘ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ Element │ │ │ │ │ Element │
|
||||
┌──│ 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 │ │ │ │
|
||||
│ Element │ │ │ mx event │ │ │ │ Element │
|
||||
│ 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 │ │ │
|
||||
│ │ │ │ └────────────────────┘ │ └────┘ │ │
|
||||
│ │ │ ┌────────────────────┐ │ │ │ Element │
|
||||
│ │ │ │ │ m.call.candidates │ │ │ App │
|
||||
│ Element │ │ │ 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 │ │ │ │
|
||||
│ Element │ │ │ mx event │ │ │ │ Element │
|
||||
│ 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 │
|
||||
└─────────────────┘ │ └─────────────────┘
|
||||
│
|
||||
│
|
||||
┌─────────────────┐ │ ┌─────────────────┐
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ Element │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Element │
|
||||
│ 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,12 +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
|
||||
@@ -69,6 +72,13 @@ class RxRoom(private val room: Room) {
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -101,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,14 +17,20 @@
|
||||
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
|
||||
@@ -36,9 +42,10 @@ 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.UserAccountDataEvent
|
||||
import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function3
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
@@ -165,6 +172,42 @@ class RxSession(private val session: Session) {
|
||||
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<UserAccountDataEvent>, 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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,19 +116,22 @@ dependencies {
|
||||
def markwon_version = '3.1.0'
|
||||
def daggerVersion = '2.25.4'
|
||||
def work_version = '2.3.3'
|
||||
def retrofit_version = '2.6.2'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
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"
|
||||
|
||||
// Network
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
|
||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||
@@ -161,6 +173,10 @@ dependencies {
|
||||
// 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'
|
||||
@@ -175,6 +191,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'
|
||||
@@ -187,4 +204,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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
||||
@@ -116,6 +117,7 @@ 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(1)
|
||||
val timelineListener = object : Timeline.Listener {
|
||||
@@ -134,11 +136,12 @@ class CommonTestHelper(context: Context) {
|
||||
|
||||
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) {
|
||||
@@ -146,11 +149,10 @@ class CommonTestHelper(context: Context) {
|
||||
}
|
||||
// Wait 3 second more per message
|
||||
await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages)
|
||||
timeline.removeListener(timelineListener)
|
||||
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
|
||||
}
|
||||
|
@@ -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) {
|
||||
@@ -57,7 +65,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
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)
|
||||
}
|
||||
|
||||
if (encryptedRoom) {
|
||||
@@ -167,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
|
||||
@@ -241,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.deviceId, eventWireContent.get("device_id"))
|
||||
assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
|
||||
|
||||
assertNotNull(event.eventId)
|
||||
assertEquals(roomId, event.roomId)
|
||||
@@ -257,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)
|
||||
}
|
||||
|
||||
@@ -274,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() {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -155,7 +160,7 @@ class KeyShareTests : InstrumentedTest {
|
||||
|
||||
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,13 +252,14 @@ 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"
|
||||
val txId = "m.testVerif12"
|
||||
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
|
||||
?: "", txId)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -35,7 +35,7 @@ import im.vector.matrix.android.common.TestMatrixCallback
|
||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -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))
|
||||
|
||||
|
@@ -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,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>
|
||||
|
@@ -23,6 +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.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
|
||||
@@ -41,6 +42,7 @@ import javax.inject.Inject
|
||||
*/
|
||||
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
|
||||
@@ -62,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
|
||||
|
@@ -33,7 +33,7 @@ data class MatrixConfiguration(
|
||||
),
|
||||
/**
|
||||
* Optional proxy to connect to the matrix servers
|
||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port)
|
||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
|
||||
*/
|
||||
val proxy: Proxy? = null
|
||||
) {
|
||||
|
@@ -89,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
|
||||
|
||||
/**
|
||||
|
@@ -34,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
|
||||
@@ -69,7 +69,7 @@ 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 /
|
||||
val hsString = hsUri.toString().ensureTrailingSlash()
|
||||
|
@@ -53,5 +53,15 @@ data class WellKnown(
|
||||
val identityServer: WellKnownBaseConfig? = null,
|
||||
|
||||
@Json(name = "m.integrations")
|
||||
val integrations: JsonDict? = null
|
||||
val integrations: JsonDict? = null,
|
||||
|
||||
@Json(name = "im.vector.riot.e2ee")
|
||||
val e2eAdminSetting: E2EWellKnownConfig? = null
|
||||
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class E2EWellKnownConfig(
|
||||
@Json(name = "default")
|
||||
val e2eDefault: 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()
|
||||
}
|
||||
|
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.legacy
|
||||
|
||||
interface LegacySessionImporter {
|
||||
|
||||
/**
|
||||
* Will eventually import a session created by the legacy app.
|
||||
* @return true if a session has been imported
|
||||
*/
|
||||
fun process(): Boolean
|
||||
}
|
@@ -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()
|
||||
|
@@ -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,9 +24,11 @@ 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
|
||||
@@ -42,8 +44,10 @@ 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.
|
||||
@@ -57,7 +61,6 @@ interface Session :
|
||||
CacheService,
|
||||
SignOutService,
|
||||
FilterService,
|
||||
FileService,
|
||||
TermsService,
|
||||
ProfileService,
|
||||
PushRuleService,
|
||||
@@ -125,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
|
||||
*/
|
||||
@@ -145,6 +154,16 @@ 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
|
||||
*/
|
||||
@@ -165,6 +184,16 @@ interface 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.
|
||||
@@ -177,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.
|
||||
*/
|
||||
|
@@ -21,7 +21,6 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
|
||||
interface AccountDataService {
|
||||
/**
|
||||
|
@@ -14,14 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.sync.model.accountdata
|
||||
package im.vector.matrix.android.api.session.accountdata
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationProvisioningContent
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
|
||||
/**
|
||||
* This is a simplified Event with just a type and a content.
|
||||
* Currently used types are defined in [UserAccountDataTypes].
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserAccountDataIntegrationProvisioning(
|
||||
@Json(name = "type") override val type: String = TYPE_INTEGRATION_PROVISIONING,
|
||||
@Json(name = "content") val content: IntegrationProvisioningContent
|
||||
) : UserAccountData()
|
||||
data class UserAccountDataEvent(
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "content") val content: Content
|
||||
)
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2019 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.accountdata
|
||||
|
||||
object UserAccountDataTypes {
|
||||
|
||||
const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list"
|
||||
const val TYPE_DIRECT_MESSAGES = "m.direct"
|
||||
const val TYPE_BREADCRUMBS = "im.vector.setting.breadcrumbs" // Was previously "im.vector.riot.breadcrumb_rooms"
|
||||
const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls"
|
||||
const val TYPE_WIDGETS = "m.widgets"
|
||||
const val TYPE_PUSH_RULES = "m.push_rules"
|
||||
const val TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning"
|
||||
const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets"
|
||||
const val TYPE_IDENTITY_SERVER = "m.identity_server"
|
||||
const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019 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.
|
||||
@@ -14,29 +14,34 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.create
|
||||
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 Invite3Pid(
|
||||
data class TurnServerResponse(
|
||||
/**
|
||||
* Required.
|
||||
* The hostname+port of the identity server which should be used for third party identifier lookups.
|
||||
* Required. The username to use.
|
||||
*/
|
||||
@Json(name = "id_server")
|
||||
val idServer: String,
|
||||
@Json(name = "username") val username: String?,
|
||||
|
||||
/**
|
||||
* Required.
|
||||
* The kind of address being passed in the address field, for example email.
|
||||
* Required. The password to use.
|
||||
*/
|
||||
val medium: String,
|
||||
@Json(name = "password") val password: String?,
|
||||
|
||||
/**
|
||||
* Required.
|
||||
* The invitee's third party identifier.
|
||||
* Required. A list of TURN URIs
|
||||
*/
|
||||
val address: String
|
||||
@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)
|
||||
}
|
||||
|
@@ -21,9 +21,11 @@ import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
@@ -82,6 +84,9 @@ data class Event(
|
||||
@Transient
|
||||
var mCryptoError: MXCryptoError.ErrorType? = null
|
||||
|
||||
@Transient
|
||||
var mCryptoErrorReason: String? = null
|
||||
|
||||
@Transient
|
||||
var sendState: SendState = SendState.UNKNOWN
|
||||
|
||||
@@ -182,6 +187,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
|
||||
@@ -200,6 +206,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
|
||||
}
|
||||
@@ -230,3 +237,23 @@ fun Event.isVideoMessage(): Boolean {
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isFileMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.getRelationContent(): RelationDefaultContent? {
|
||||
return if (isEncrypted()) {
|
||||
content.toModel<EncryptedEventContent>()?.relatesTo
|
||||
} else {
|
||||
content.toModel<MessageContent>()?.relatesTo
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isReply(): Boolean {
|
||||
return getRelationContent()?.inReplyTo?.eventId != null
|
||||
}
|
||||
|
@@ -58,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"
|
||||
@@ -67,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"
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.file
|
||||
|
||||
import android.net.Uri
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
@@ -31,26 +32,58 @@ interface FileService {
|
||||
* Download file in external storage
|
||||
*/
|
||||
TO_EXPORT,
|
||||
|
||||
/**
|
||||
* Download file in cache
|
||||
*/
|
||||
FOR_INTERNAL_USE,
|
||||
|
||||
/**
|
||||
* Download file in file provider path
|
||||
*/
|
||||
FOR_EXTERNAL_SHARE
|
||||
}
|
||||
|
||||
enum class FileState {
|
||||
IN_CACHE,
|
||||
DOWNLOADING,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file.
|
||||
* Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision.
|
||||
* You can pass the eventId
|
||||
* Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
|
||||
*/
|
||||
fun downloadFile(
|
||||
downloadMode: DownloadMode,
|
||||
id: String,
|
||||
fileName: String,
|
||||
mimeType: String?,
|
||||
url: String?,
|
||||
elementToDecrypt: ElementToDecrypt?,
|
||||
callback: MatrixCallback<File>): Cancelable
|
||||
|
||||
fun isFileInCache(mxcUrl: String, mimeType: String?): Boolean
|
||||
|
||||
/**
|
||||
* Use this URI and pass it to intent using flag Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
* (if not other app won't be able to access it)
|
||||
*/
|
||||
fun getTemporarySharableURI(mxcUrl: String, mimeType: String?): Uri?
|
||||
|
||||
/**
|
||||
* Get information on the given file.
|
||||
* Mimetype should be the same one as passed to downloadFile (limitation for now)
|
||||
*/
|
||||
fun fileState(mxcUrl: String, mimeType: String?): FileState
|
||||
|
||||
/**
|
||||
* Clears all the files downloaded by the service
|
||||
*/
|
||||
fun clearCache()
|
||||
|
||||
/**
|
||||
* Get size of cached files
|
||||
*/
|
||||
fun getCacheSize(): Int
|
||||
}
|
||||
|
@@ -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.matrix.android.api.session.file
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
|
||||
/**
|
||||
* We have to declare our own file provider to avoid collision with apps using the sdk
|
||||
* and having their own
|
||||
*/
|
||||
class MatrixSDKFileProvider : FileProvider() {
|
||||
override fun getType(uri: Uri): String? {
|
||||
return super.getType(uri) ?: "plain/text"
|
||||
}
|
||||
}
|
@@ -16,9 +16,20 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.group
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact within a group.
|
||||
*/
|
||||
interface Group {
|
||||
val groupId: String
|
||||
|
||||
/**
|
||||
* This methods allows you to refresh data about this group. It will be reflected on the GroupSummary.
|
||||
* The SDK also takes care of refreshing group data every hour.
|
||||
* @param callback : the matrix callback to be notified of success or failure
|
||||
* @return a Cancelable to be able to cancel requests.
|
||||
*/
|
||||
fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
||||
|
@@ -32,7 +32,12 @@ data class HomeServerCapabilities(
|
||||
/**
|
||||
* Default identity server url, provided in Wellknown
|
||||
*/
|
||||
val defaultIdentityServerUrl: String? = null
|
||||
val defaultIdentityServerUrl: String? = null,
|
||||
/**
|
||||
* Option to allow homeserver admins to set the default E2EE behaviour back to disabled for DMs / private rooms
|
||||
* (as it was before) for various environments where this is desired.
|
||||
*/
|
||||
val adminE2EByDefault: Boolean = true
|
||||
) {
|
||||
companion object {
|
||||
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L
|
||||
|
@@ -17,6 +17,7 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.profile
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
@@ -35,12 +36,27 @@ interface ProfileService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current dispayname for this user
|
||||
* Return the current display name for this user
|
||||
* @param userId the userId param to look for
|
||||
*
|
||||
*/
|
||||
fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the display name for this user
|
||||
* @param userId the userId to update the display name of
|
||||
* @param newDisplayName the new display name of the user
|
||||
*/
|
||||
fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the avatar for this user
|
||||
* @param userId the userId to update the avatar of
|
||||
* @param newAvatarUri the new avatar uri of the user
|
||||
* @param fileName the fileName of selected image
|
||||
*/
|
||||
fun updateAvatar(userId: String, newAvatarUri: Uri, fileName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Return the current avatarUrl for this user.
|
||||
* @param userId the userId param to look for
|
||||
|
@@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.api.session.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.session.room.call.RoomCallService
|
||||
import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
|
||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
@@ -47,6 +48,7 @@ interface Room :
|
||||
StateService,
|
||||
UploadsService,
|
||||
ReportingService,
|
||||
RoomCallService,
|
||||
RelationService,
|
||||
RoomCryptoService,
|
||||
RoomPushRuleService {
|
||||
|
@@ -34,13 +34,6 @@ interface RoomDirectoryService {
|
||||
publicRoomsParams: PublicRoomsParams,
|
||||
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id, or room alias
|
||||
*/
|
||||
fun joinRoom(roomIdOrAlias: String,
|
||||
reason: String? = null,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Fetches the overall metadata about protocols supported by the homeserver.
|
||||
* Includes both the available protocols and all fields required for queries against each protocol.
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
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.util.Cancelable
|
||||
@@ -104,5 +105,13 @@ interface RoomService {
|
||||
searchOnServer: Boolean,
|
||||
callback: MatrixCallback<Optional<String>>): Cancelable
|
||||
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
|
||||
/**
|
||||
* Return a live data of all local changes membership that happened since the session has been opened.
|
||||
* It allows you to track this in your client to known what is currently being processed by the SDK.
|
||||
* It won't know anything about change being done in other client.
|
||||
* Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action
|
||||
*/
|
||||
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
|
||||
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String): Room?
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {
|
||||
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
|
||||
*/
|
||||
data class RoomSummaryQueryParams(
|
||||
val roomId: QueryStringValue,
|
||||
val displayName: QueryStringValue,
|
||||
val canonicalAlias: QueryStringValue,
|
||||
val memberships: List<Membership>
|
||||
@@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
|
||||
|
||||
class Builder {
|
||||
|
||||
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
|
||||
var memberships: List<Membership> = Membership.all()
|
||||
|
||||
fun build() = RoomSummaryQueryParams(
|
||||
roomId = roomId,
|
||||
displayName = displayName,
|
||||
canonicalAlias = canonicalAlias,
|
||||
memberships = memberships
|
||||
|
@@ -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.api.session.room.call
|
||||
|
||||
/**
|
||||
* This interface defines methods to handle calls in a room. It's implemented at the room level.
|
||||
*/
|
||||
interface RoomCallService {
|
||||
/**
|
||||
* Return true if calls (audio or video) can be performed on this Room
|
||||
*/
|
||||
fun canStartCall(): Boolean
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.room.members
|
||||
|
||||
sealed class ChangeMembershipState() {
|
||||
object Unknown : ChangeMembershipState()
|
||||
object Joining : ChangeMembershipState()
|
||||
data class FailedJoining(val throwable: Throwable) : ChangeMembershipState()
|
||||
object Joined : ChangeMembershipState()
|
||||
object Leaving : ChangeMembershipState()
|
||||
data class FailedLeaving(val throwable: Throwable) : ChangeMembershipState()
|
||||
object Left : ChangeMembershipState()
|
||||
|
||||
fun isInProgress() = this is Joining || this is Leaving
|
||||
|
||||
fun isSuccessful() = this is Joined || this is Left
|
||||
|
||||
fun isFailed() = this is FailedJoining || this is FailedLeaving
|
||||
}
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.members
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
@@ -63,6 +64,12 @@ interface MembershipService {
|
||||
reason: String? = null,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Invite a user with email or phone number in the room
|
||||
*/
|
||||
fun invite3pid(threePid: ThreePid,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Ban a user from the room
|
||||
*/
|
||||
|
@@ -17,8 +17,8 @@ package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
data class EventAnnotationsSummary(
|
||||
var eventId: String,
|
||||
var reactionsSummary: List<ReactionAggregatedSummary>,
|
||||
var editSummary: EditAggregatedSummary?,
|
||||
var pollResponseSummary: PollResponseAggregatedSummary?,
|
||||
var reactionsSummary: List<ReactionAggregatedSummary> = emptyList(),
|
||||
var editSummary: EditAggregatedSummary? = null,
|
||||
var pollResponseSummary: PollResponseAggregatedSummary? = null,
|
||||
var referencesAggregatedSummary: ReferencesAggregatedSummary? = null
|
||||
)
|
||||
|
@@ -17,10 +17,12 @@
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Represents the membership of a user on a room
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Membership(val value: String) {
|
||||
|
||||
NONE("none"),
|
||||
|
@@ -35,4 +35,18 @@ data class PowerLevelsContent(
|
||||
@Json(name = "users") val users: MutableMap<String, Int> = HashMap(),
|
||||
@Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
|
||||
@Json(name = "notifications") val notifications: Map<String, Any> = HashMap()
|
||||
)
|
||||
) {
|
||||
/**
|
||||
* Alter this content with a new power level for the specified user
|
||||
*
|
||||
* @param userId the userId to alter the power level of
|
||||
* @param powerLevel the new power level, or null to set the default value.
|
||||
*/
|
||||
fun setUserPowerLevel(userId: String, powerLevel: Int?) {
|
||||
if (powerLevel == null || powerLevel == usersDefault) {
|
||||
users.remove(userId)
|
||||
} else {
|
||||
users[userId] = powerLevel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user